Flutter 动态化在最右 App 中的实践

阅读数:1 2020 年 1 月 7 日 10:31

Flutter动态化在最右App中的实践

1、写在前⾯面

Flutter⾃自诞⽣生便便备受关注,其⾼高效的⾃自渲染技术注定要在性能和体验上优于在这之前的跨端⽅方案,美中不不⾜足 的是⽬目前 Flutter 不不具备像 Hybrid、RN、Weex 等拥有的动态更更新能⼒力力,官⽅方在 2019 年年的 Roadmap⾥里里⾯面原本有 ⽀支持动态化的想法,但后来⼜又出于性能、安全等⽅方⾯面的考量量⽽而放弃了了。 最右去做 Flutter 动态化的⽬目的是为技术选型提供多样的选择,在⼀一些使⽤用 H5 但交互性强的场景,或者使⽤用原 ⽣生但⾮非核⼼心的独⽴立场景,提供更更优的⽅方案。今天主要跟⼤大家分享最右 App 在实现 Flutter 动态化的过程中的⼀一些 经验教训。 下⾯面列列出本篇⽂文章的⼤大纲和思路路,以便便于⼤大家更更好地理理解:

Flutter动态化在最右App中的实践

2、先说 Android

2.1 实现思路路

实现动态化,就是实现指定路路径的代码和资源的加载,并确保代码能正常执⾏行行。
要达成这个⽬目标,修改 Engine 是必然的。我们先将 Engine 的源码 Clone 下来,配置好 Engine 的编译环境,具 体可参考官⽅方 Wiki[1]。 同时我们必须先了了解 Flutter 本身的启动流程,了了解系统本身到底是如何⼯工作的,不不清楚的同学可以查看 Gityuan 的博客——深⼊入理理解 Flutter 引擎启动 [2]。这篇⽂文章的第 3.3.1⼩小节可以获知系统的资源路路径信息保存在 Settings 结构体中,其实可执⾏行行代码路路径也是保存在这个结构体中,具体可以看下图。

Flutter动态化在最右App中的实践

理理解系统的⼯工作流程之后,我们要实现这个⽬目的,必须找到恰当的时机,将 Settings 中保存的代码路路径和资源 路路径修改成指定的路路径。然后编译 Engine,⽣生成flutter.jar。Engine 的编译可参考官⽅方 Wiki[3]。

2.2 实现原理理

在实现思路路中,我们要解决这个问题的核⼼心就在于找到这个恰当的时机。我们选择的是在 platform_view_android_jni.cc 的 AttachJNI,AndroidShellHolder 创建之前的时机。
Flutter动态化在最右App中的实践

在重新编译 Engine 之后,我们将⽣生成的flutter.jar 预置到主⼯工程中去,这样主⼯工程就有了了动态加载 Flutter 代码 和资源的能⼒力力。

2.3 工作流程

Flutter动态化在最右App中的实践

2.3.1 编译阶段

Android 端⾛走正常的flutter build 即可,将编译产物 libapp.so 和flutter_assets 打包成⼀一个资源包,上传 CDN。

2.3.2 加载阶段

在加载阶段之前,还需要有⼀一个资源包下载和安全校验的环节,在此不不做讨论。当我们拿到完整的资源包之
2.3.1 编译阶段
2.3.2 加载阶段
后,将其路路径传递到 Flutter Engine 层。 Android 端是从 Flutter.createView 开始向下透传路路径,⼤大致的经过 是:Flutter.createView -> FlutterNativeView 构造⽅方法 -> FlutterJNI.attachToNative -> FlutterJNI.nativeAttach -> AttachJNI,在 Engine 层 platform_view_android_jni.cc 的 AttachJNI 处修改 Flutter 默认的 Settings,将 Settings 的 application_library_path 指向⾃自定义路路径下的 libapp.so, 将 assets_path 指向⾃自定义路路径下的 flutter_assets。⾄至此,Android 侧便便能加载到⾃自定义的代码和资源了了。

3、再说 iOS

3.1 实现思路路

按道理理iOS 上也可以采取跟 Android 同样的思路路,但是由于苹果开发者协议的规定,不不允许动态更更新、运⾏行行可 执⾏行行代码;所以在 Flutter 资源的处理理上,我们可以采⽤用同 Android⼀一样的思路路,但是对代码的处理理,我们需要 寻找新的⽅方案。回顾之前的这些跨端⽅方案,我们可以参照 RN 的实现,只不不过 N 不不再是 Native 了了,⽽而是 Flutter。RN 是通过 JS 控制 Native 渲染,我们要实现的是通过 JS 控制 Flutter 渲染。 开发者⼀一定要⽤用 JS 去开发吗? 腾讯 TGIF-iMatrix 开源的 MXFlutter[4] 便便是⼀一个基于 JS 的 Flutter 动态化框架。它 ⽤用极类似 Dart 的开发⽅方式,通过编写 JavaScript 代码,来开发 Flutter 应⽤用。 能不不能对 Flutter 开发者透明? 最右探索了了另外⼀一条路路,Flutter 提供了了⼀一个强⼤大的⼯工具 dart2js,借助这个⼯工具 我们可以实现编译阶段将 Dart 代码编译成 JS。 为此,我们还需要研发⼀一套框架,⽀支持动态下发的 JS 控制 Flutter 渲染,并让 Flutter 回传事件,JS 实现所有的 业务逻辑,来实现动态化。

3.2 实现原理理

我们有两⽅方⾯面的事情需要完成,⼀一⽅方⾯面是修改 Engine 实现⾃自定义资源加载,这部分的思路路同 Android 端是⼀一致 的,唯⼀一的区别是在 iOS 侧只需要⽀支持⾃自定义资源的加载,这部分就不不再赘述了了。另⼀一⽅方⾯面就是实现⼀一套类 RN 的框架,这部分相对⽐比较复杂,后⾯面会详细介绍。 我们确定了了这套框架⼤大致的⼯工作流程,JS 侧承载所有的业务逻辑,通过构建与业务逻辑匹配的 Widget 虚拟 树,将其数据化传递给 Flutter,Flutter 解析这个 UI 描述,构建出真实的 Widget Tree;这个框架必须有三部 分:由 Flutter SDK 的同名镜像类和业务代码⼀一起编译⽣生成 app.js,我们称之为 Client 部分;在 Flutter 侧⽤用于 UI 渲染和事件接收,我们称之为 Host 部分;还有⼀一部分是连接两端的桥梁梁,不不仅需要辅助实现 JS 和 Flutter 的双 向通信,还为 JS 侧提供⼀一些必要的机制,我们称之为 Native 部分。 我们将修改过的 Engine 编译出 Flutter.framework,以及框架 Host 部分的代码(App.framework),预置到主⼯工 程中,这相当于在主 App 中给 Flutter App 提供了了环境⽀支撑。 把 app.js 和flutter_assets 打包成⼀一个资源包下发到端上,当⽤用户启动某个 Flutter App 时,主 App 会将资源包的 路路径传递给 Flutter Engine,并且启动了了预置的 App.framework,⽽而且主 App 还会加载 app.js,在 JS 与 Flutter 建 ⽴立通信之后,实现 Flutter App 的运转。

3.3、iOS 端动态化框架——JS2Flutter

下⾯面是整个框架的结构图:
Flutter动态化在最右App中的实践
Flutter动态化在最右App中的实践

3.3.1 Client 部分

3.3.1.1 Flutter SDK Widget 组件镜像

Widget 组件主要是提供 Flutter 各种组件的镜像,为了了便便于 Widget 虚拟树的构建,每个 Widget 都有数据化成 Json 的能⼒力力,以 MaterialButton 为例例,toJson 时将 splashColor、height 等信息存⼊入 Json,Host 侧在收到 onPressed 事件时会将事件传递给 Client 侧的镜像,由镜像的 MaterialButton 通知业务 onPressed 事件被触发。
Flutter动态化在最右App中的实践

3.3.1.2 UI 数据化

UI 数据化的过程指的是在 JS 侧构建出虚拟树,然后将这棵树通过 Json 数据化之后传递给 Flutter。 为什什么要 UI 数据化? 我们的逻辑都是在 JS 侧控制的,但是真实的绘制能⼒力力是在 Flutter 侧,我们要想完成 JS 控 制 Flutter 去渲染,就必须告诉 Flutter 我们想要渲染的是什什么,这个过程就需要⽤用 Json 来描述了了。 怎样实现 UI 数据化? 每个节点都有数据化⾃自⼰己和⼦子树的能⼒力力,就能从根节点完成数据化。对于树的构建,我 们参照了了Flutter 的实现,根据不不同的场景,提供了了⼀一些基础的 Widget,如 StatelessWidget、 StatefulWidget、SingleChildWidget、NoChildWidget、DeferChildWidget、MultiChildWidget 和 MapChildWidget 等。

3.3.1.3 通信机制

通信机制是整个框架的基⽯石,Client 部分主要是跟 Native 双向通信,要在 JS 侧建⽴立异步、同步、Vsync 等机 制,当然这部分需要 Native 部分的配合。 ⼤大部分消息可能是⽆无需关注返回结果的,⽐比如通知 Flutter 侧刷新 UI 数据,但有部分场景也需要返回结果,这 时候就需要异步、同步机制了了。举个使⽤用场景,我需要 showTimePicker,然后获取到选择的时间,系统的返 回值也是⼀一个 Future,这种场景我们就需要异步机制。同步其实是相对于 JS 侧的,针对于 JSWorkThread, 这是⼀一个与 UI 绘制⽆无关的线程,所以它的阻塞并不不影响渲染和事件接收,所以这个同步也仅仅是 JS 侧的同 步,有些⽅方法是必须需要同步机制才能保证正确性的,⽐比如你要通过 TextPainter 来测量量⽂文字的⾼高度。Vsync 的机制主要是⽤用来⽀支持 CustomPainter 和⼩小游戏能⼒力力的,它们出现在那些直接通过 Canvas⾃自绘的场景。 除开这三⼤大部分外,还有⼀一些机制的⽀支持,⽐比如 WidgetBinding、MethodChannel 和 EventChannel 的⽀支持, 它们的实现思路路跟 Widget 组件都⼀一样,在 Client 侧都是镜像,真身还是在 Host 部分。Client 部分的代码最终会 被依赖编译到 app.js⽂文件中去。
Flutter动态化在最右App中的实践

3.3.2 Native 部分

Native 部分的 XCJSRuntime 创建了了独⽴立的 JSWorkThread,并通过 RunLoop 建⽴立消息循环,在这个线程完成 了了app.js 的加载和执⾏行行。通过 CADisplayLink,给 JS 侧提供了了Vsync 机制,Client 部分的 SchedulerBinding 便便是 基于此 Vsync 机制去实现的。同时也实现 setTimeout 和 setInterval 等,这个主要是为了了解决 dart2js 之后, Timer 在 js 侧没有 setTimeout 和 setInterval 的问题。

3.3.3 Host 部分

这部分主要是解析 Client 传递过来的数据,构建出真实的 Widget Tree,当接受到⽤用户事件之后,将事件传递 给 Client 对应的镜像。

3.3.3.1 数据解析

数据解析包括两类,⼀一类是包含 Widget 信息的数据,主要是根据携带过来的类型解析成对应的 Widget,⽐比如 当识别到传过来的是 MaterialButton 之后,在 Host 会构造真实的 MaterialButton,并填充其 splashColor、 height 等。⼀一类是通过 Canvas 直接绘制的指令,这类数据根据⾃自⼰己定义的协议解析出对应的 Canvas 命令即 可,如 save、restore、translate、rotate、drawImageRect 等。

3.3.3.2 通信机制

Host 侧的通信机制主要是通过 MethodChannel 跟 Native 双向通信,从⽽而实现 JS 到 Flutter,Flutter 到 JS 的双向 通信。

3.4 工作流程

Flutter动态化在最右App中的实践
Flutter动态化在最右App中的实践

3.4.1 编译阶段

借助 dart2js 这个强⼤大的⼯工具,将业务代码和框架的 Flutter SDK 镜像代码编译成 app.js,跟flutter_assets 打包 成⼀一个资源包,上传 CDN。

3.4.2 加载阶段

加载分为两部分:⼀一部分是flutter_assets 资源的加载,⼀一部分是 app.js 的加载;我们给 FlutterDartProject 添加 了了⼀一个 setAssetsPath⽅方法,其作⽤用就是指定 Settings 的 assets_path,在 FlutterEngine 的 initWithName 构造⽅方 法中指定这个 FlutterDartProject 即可。在启动 FlutterEngine 之后,上层通过 XCJSRuntime 开始加载 app.js。

3.4.3 运⾏行行阶段

XCJSRuntime 在 JSWorkThread 线程中加载 app.js,并在加载完成之后将 Widget Tree 数据发送到 Native 侧,然后经过 MethodChannel 传递给框架 Host 部分,框架先对数据进⾏行行解析,还原成对应的 Widget,从⽽而构建出 真实的 Widget Tree,⾄至此便便完成了了⻚页⾯面的展示,当 Flutter 接收到事件之后,会回传给 JS 侧,由 JS 侧处理理事件 的响应。

4、爬过的坑

在实现 JS2Flutter 框架过程中遇到了了很多⼤大⼤大⼩小⼩小的坑,挑⼏几个印象⽐比较深刻的坑跟⼤大家分享。

4.1 Widget Tree 状态同步

这个主要是针对 StatefulWidget,StatefulWidget 的应⽤用⾮非常⼴广,⽽而且经常出现在⼀一些较为复杂的场景,由于 最原始的 UI 描述数据来⾃自于 JS 侧,当 StatefulWidget 对应的 State 触发刷新时,JS 侧会重新构建⼦子树,传递给 Flutter,在 Host 侧解析新的数据,并重新渲染。由于整棵树的数据化结构是⼀一个⼤大的 Map,从根节点开始进 ⾏行行遍历创建节点,StatefulWidget 的数据其实依赖于⽗父节点传给他的数据,问题就出现在这⾥里里,试想⼀一下如果 有两个 StatefulWidget 嵌套,⼦子 StatefulWidget 先触发了了⾃自身 State 的刷新,它的数据在 JS 侧已经变了了,如果这 个时候外层的 StatefulWidget 触发⼀一次 build(不不由 JS 主动触发,⽐比如进去⼀一个新的⻚页⾯面,系统会触发⼀一次 build),⼦子 StatefulWidget 的状态会被刷新成原始状态,因为⼦子 StatefulWidget 本身的数据刷新,并没有将这 部分数据同步到整个树结构中去,这是框架初期犯的⽐比较⼤大的⼀一个逻辑错误。

4.2 延迟构造的 Widget 嵌套 StatefulWidget

延迟构造⼦子树的 Widget 很多,⽐比如 Builder、LayoutBuilder 等,在框架 Client 端我们称它们为 DeferChildWidget,这类 Widget 的实现基本上都是在 Host 侧预先⽤用⼀一个 StatefulWidget 占坑,然后在占坑的 StatefulWidget 的 State 的 initState 时机,向 JS 侧请求⼦子树的数据,JS 侧构建好⼦子树数据后,再回传给占位的 StatefulWidget,刷新其 State,这时候才开始触发真实⼦子树的构建。 ⽽而前期对于 StatefulWidget 的实现,虽然在 Host 侧有与之对应的 StatefulWidgetHost(继承⾃自真实的 StatefulWidget)来实现⾃自定义的 StatefulWidget,但并没有⽤用 Host 侧真实的时机同步给 JS 侧。框架 Client 侧 回调给 StatefulWidget 的 State 的 initState 和 dispose 都是在 JS 侧根据虚拟树构建、销毁时回调的。 所以当 DeferChildWidget 嵌套⼀一个 StatefulWidget 的时候,这⾥里里⾯面就有⼀一个时序问题,假如 initState 中有⼀一个 异步获取数据(如:从 SharedPreference 获取⼀一个状态),拿到数据后更更新状态的操作,⽽而这些都先于 DeferChildWidget 在 Host 侧预占坑的 StatefulWidget 触发其真实⼦子树的构建的时候,问题就暴暴露露了了。
其实,实现这样⼀一个框架还遇到了了很多有挑战的问题,例例如:⼩小游戏如何⾼高效绘制?如何提升通信效率等 等?在此就不不展开讨论了了。

5、结束语

Flutter 的流⾏行行已经势不不可挡,相信有很多开发者已经在 Flutter 的动态化⽅方向上做尝试,本⽂文分享了了最右 App 在 实现 Flutter 动态化的过程中的⼀一些经验教训,希望对⼤大家有所帮助。最右 App 所采⽤用的 Flutter 版本是 1.9.1+hotfix.6,本⽂文所讨论的技术都是基于此版本,Gityuan 的⽂文章——深⼊入理理解 Flutter 引擎启动 [2] 是基于 Flutter1.5 的源码进⾏行行分析,源码细节略略微有些差异,启动过程是⼀一致的。

6、参考⽂文献

[1]:Engine 编译环境构建 Wiki https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-developmentenvironment [2]: 深⼊入理理解 Flutter 引擎启动 http://gityuan.com/2019/06/22/flutter_booting/ [3]:Engine 编译 Wiki https://github.com/flutter/flutter/wiki/Compiling-the-engine [4]:MXFlutter https://github.com/TGIF-iMatrix/MXFlutter

评论

发布