原文链接:满帮动态化 Flutter 框架“Thresh”,当初开源了
一、前言
挪动端技术栈自诞生以来,其双端开发成本和公布效率始终广受诟病。为了解决这些问题,前端跨端技术始终在一直尝试,心愿能一次开发、多端运行并且能做到疾速公布。期间经验了多个技术倒退阶段。
第一阶段:以 H5 为代表,基于 webview 渲染
只需一次开发即可运行在双端,解决了开发效率低下的问题。然而 webview 存在重大的性能问题,用户的交互体验相比 Native 渲染有显著差距。
第二阶段:以 RN 和 Weex 为代表,前端技术栈开发,Native 渲染
这些计划应用前端技术开发,最终映射到 Native 组件渲染,用户体验相比 H5 计划有了微小的晋升。然而这一阶段的计划同样存在有余。因为框架的渲染最终还是依赖双端 Native 组件,存在双端体验不一致性和平台兼容问题,极其状况下开发成本甚至超过双端 Native 开发。
第三阶段:Flutter,自绘引擎渲染
Google 基于 Skia 渲染引擎,推出了 Flutter 跨平台框架,反对了 Android/iOS/Web 三个平台(尤其 2.0 的公布反对了全平台)。
基于自绘引擎,Flutter 抹平了各个平台的差别,真正做到了一处开发,多端运行。业内对于 Flutter 彻底解决跨端开发的问题也寄予厚望。然而 Flutter 也并非完满,其动静能力有余,无奈像 H5、RN 等技术一样疾速公布。
为了解决动静能力有余的问题,满帮大前端团队从 2019 年开始对 Flutter 动态化能力进行摸索,自研了动态化 Flutter 框架,在外部一直优化迭代,已上线 20+ 页面,包含外围页面订单详情、货主货源详情、导航地图等等,并且于 2020 年底进行了开源。
二、Flutter 动态化的思考
Thresh 我的项目推出的初心是为了能提供一种基于 Flutter 的齐全跨端动态化计划,性能能达到甚至优于 React Native,再加上其多端渲染一致性以及行将推出的 Google Fuchsia 零碎默认开发语言为 Flutter,都表明 Thresh 将来将会充斥想象力。
2.1、动态化常见计划
实现 Flutter 的动态化,通常须要思考以下几点:
- Flutter 编译产物替换
Google 本来打算在 2019 年推出 Code Push 计划,起初放弃了,次要两个起因:违反利用商店的规定和平安方面思考;但目前 android 是能够通过产物替换来做到动态化,iOS 端则无奈做到。
- 组件化搭建
通过 Dart 来定义局部外围通用组件,在平台下发已有的组件列表拼装的页面 JSON,端上再通过解析渲染成页面。这种计划能满足轻交互场景,但只能反对无限动态性。
- 自定义 Dart 转换 + 动静逻辑映射
通过自定义一套 Dart 标准以及通过转换器生成 JSON 来做到动静更新,性能损失小,然而逻辑动态性须要提前预埋,且前端开发同学须要肯定的学习老本。
- 自定义 DSL+ 依赖 JS 引擎的动静执行
相似于 RN/Weex,通过自定义动态化 UI 形容 + JS 引擎的解释运行转换思路,最终构建成页面和执行动静逻辑。这个计划对于前端开发十分敌对,零学习老本,然而因为在 JS 引擎运行,会有一些性能损耗。
2.2、Thresh 的抉择
满帮的理论应用场景,业务疾速迭代,须要 Android 和 iOS 都要反对动态性,所以产物替换的思路不能齐全解决问题。随后又思考应用组件化思路,拼接多个业务组件尽管能搭建出页面,然而弊病也很显著,简单交互逻辑时无奈实现。另外自定义 Dart 形容 UI 计划尽管满足了动静更新的要求,然而逻辑动态性仍旧不强,而且 Dart 开发对于前端开发同学有肯定的学习老本。
最终,综合思考了开发效率、学习老本、多端性能和一致性等因素,咱们抉择了自定义 JS 形容 UI + JS 引擎的解释运行转换思路,类 React 语法结构,开发语言应用 JS/TS。
三、实现原理
3.1、构建 Dart 页面原理
在 Flutter 中形容视图组成的根本单位是 Widget,每一个 Widget 只蕴含以后部件的配置信息,它是一个轻量的、可被高效创立并销毁的数据结构。而许许多多的 Widgets 组合在一起,构建出了一个蕴含视图所有信息的 WidgetTree。之后 Flutter 会从 WidgetTree 中生成 ElementTree,再由 ElementTree 生成 RenderObjectTree。ElementTree 中的 Element 会同时持有其对应的 Widget 与 renderObject。
三棵树中,WidgetTree 会被频繁创立于销毁,然而 ElementTree 和 RenderObjectTree 只会在产生状态扭转的时候才会扭转,ElementTree 负责元素的更新与 diff,RenderObjectTree 则负责理论的布局与绘制。
外围思路是把 Flutter 的页面渲染逻辑中的三棵树中的第一棵树 Widget,通过 JS 来结构。这其中要实现 JS 与 Flutter 层实现根底组件映射,再通过 JS 引擎来生成 UI 形容,并传递给 Dart 层的 UIEngine,UIEngine 把 UI 形容转换为 Flutter 控件,最终渲染成页面。
Thresh 框架实现了罕用根底组件的定义与开发,能撑持 95% 以上业务场景的接入,语法定义规定反对 React,对前端开发人员零老本接入。现反对的组件列表以及其局部属性如下
3.1.1、Flutter 初始化
Flutter 是由 main() 函数开始程序执行的,次要实现以下几个工作:
- 建设与 Native 之间的通信渠道 MethodChannel 以保障所有的通信都可能被接管和发送;
- 建设接管到音讯时的所有解决办法的散发渠道,以保障所有非法的通信都可能在 Flutter 中被正确处理,同时通过 MethodChannel 向 JS 发送以后设施的媒介数据;
- 注册拦挡函数,以便在接管到渲染 JSON 数据后将 JSON 转换为 Widget;
- 最初建设 Flutter App 的初始承载页面,该页面在接管到 JS 发送显示页面的音讯之前将会始终处于期待状态;同时向 JS 发送 ready 音讯,示意 Flutter 环境已筹备实现,能够显示页面。
3.1.2、生成 WidgetTree
根据 Flutter 中对 Widget 注册的所有拦挡函数,JS 中会提供一套与之绝对应的原子组件,以便在两种不同的 DSL 之间进行组件的相互转换。在 JS 中 UI 的构建通过 JSX 实现,借鉴了 React 的写法。
通过在 JS 中构建 UI 的形容层,再将 UI 形容转换为 JSON 格局字符串,经由 Native 发送到 Flutter,由 Flutter 对 JSON 字符串进行解析后创立对应的 WidgetTree 并执行后续渲染操作。
3.1.3、JS 与 Flutter 通信
在 JS 代码执行之前,Native 会向 JS 代码的执行环境中注册两个通信办法,一个为 JS 向 Flutter 传递音讯的通道,另一个则是 Flutter 向 JS 传递音讯的通道。通过这两个通道,就能够实现所有数据在 JS 与 Flutter 之间的流转(前面 3.2 章节会具体介绍)。
3.1.4、构建 Flutter 页面
对于当实现所有链路的数据转换后就会拿到 ModelTree & WidgetTree,ModelTree 会持有并缓存 WidgetTree,最终构建一个 Widget 页面并渲染显示。页面构建渲染流程次要是:
Flutter 接管到渲染 JSON 数据后,会通过递归遍历的形式从最底层开始,将每一个独立的渲染数据节点解析为 Model 对象。Model 将会持有所有的渲染数据,同时会关联本人的父节点;同时 Model 会携带所有的渲染数据,通过 Widget 拦挡函数生成其对应的 Widget 实例,并持有该 Widget 实例。
比方,JS 中的 <Container />
组件在 Flutter 中通过拦挡函数将会被创立为一个名叫 DFContainer 的 widget 实例。DFContainer 等 widgets 是应用 Flutter 提供的原子组件封装的一套自定义组件。
当通过 model 创立 Widget 时,如果发现其 isStateful = true
,则会在该 Widget 实例外层包裹一个 StatefulWidget,同时让 model 持有该 StatefulWidget 及其 state,以便之后进行更新操作。也就是说,如果一个 model 具备 isStateful = true
,则其会同时领有 Widget & statefulWidget & state 的个性。
在遍历过程中,原先的 JSON 数据会被转换为两个树 —— ModelTree & WidgetTree。其中 WidgetTree 中的每个节点都会被 ModelTree 中对应的节点所持有。
对于首次显示的页面来说,会应用被创立的 WidgetTree 间接替换初始化时创立的承载页面的内容;而非首页则会间接通过 Navigator.push(),应用 WidgetTree 创立并显示一个新页面。整个流程如下图:
3.2、通信机制
JS 与 Flutter 是依赖于 Native 又齐全独立的两端:JS 中的数据运算与流转不会间接影响到 Flutter 页面的渲染;Flutter 的渲染过程也不会阻塞 JS 的代码执行。
为了让齐全独立的两者产生分割,咱们找到了一个既能与 JS 产生分割,又能与 Flutter 传递音讯的媒介 —— Native. 通过将一个音讯从一端传递给 Native,再由 Native 残缺传递给另一端,就实现了 JS 与 Flutter 之间的通信。
动态化 Flutter 框架次要由这三局部形成,每一部分都解决不同的逻辑和绑定事件通信来更新渲染页面、事件响应,其外围渲染通信流程:Flutter ⇋ Native ⇋ JS。
3.2.1、搭建三端通信链路
Flutter 初始化时,Flutter 会与 Native 通过 methodChannel 建设通信关系,methodChannel 是一条双向通信的链路,既能够在 Flutter 中接管到 Native 的音讯,也能够被动向 Native 收回音讯。
同时,Native 在执行 JS 代码之前会向 JS 的 context 中注入一个办法,咱们将这个办法命名为 methodChannel_js_call_flutter,用来使 JS 可能向 Flutter 传递音讯。因而,在 Flutter 动态化中的通信链路如下图。
从下面两个链路中会发现,JS 到 Native 的音讯是能够顺利达到 Flutter;然而 Flutter 到 JS 没有间接的的通信链路,在 Native 中断掉了。为了解决这个问题,JS 会在 context 中裸露一个名为 methodChannel_flutter_call_js 的办法,该办法的参数即为音讯内容,这样 Native 就可能间接调用该办法将消息传递到 JS。
3.2.2、“半双工”通信过程
在 Thresh 中,简直所有的三端通信需要都是“半双工”的。此处的“半双工”指的是,当一方作为消息传递方时,无奈通过以后传递音讯的通道取得音讯接受方的反馈。这就示意当传递方发送出一条音讯后就会完结本人的通信行为,它们不须要去关怀本人是否会失去反馈,而实际上也不会有任何反馈。
基于以上状况,Thresh 中的所有通信链路都会应用这种模式进行通信:消息传递方只须要传递数据而不须要关怀回调,音讯接管方只须要解决数据而不须要返回处理结果。这种模式对于逾越三端的通信来说更便于管理和束缚,也使得 Native 成为了一个齐全的数据中转站,否则 Native 除了须要传送数据外,还须要处理结果的反馈工作。即【数据传递方】->【数据直达方】->【数据接管方】是单向的。
然而并不是所有的通信都不须要反馈,例如与 Native 通信的双端通信链路 bridge,在向 Native 收回通信音讯后须要取得 Native 的处理结果。对于这种状况,简略粗犷的单向通信将无奈间接满足需要。但如果换成携带回调的“全双工”通信,从而可能在同一个通信通道上实现后果的接管,将会毁坏原有的通信模式,也为通信的治理减少了难度。
为了解决在“半双工”通信模式上的通信反馈问题,咱们通过在传递方为每一个须要反馈的通信加上标识符,再将反馈解决办法通过标识符缓存;当接管方解决实现后,携带标识符通过另一个通信通道将处理结果作为一个新的消息传递给本来的传递方后(在这个新的通道中,本来的数据传递和接管方将会调换身份),传递方会依据标识符在缓存中查找到解决办法并执行解决逻辑。
3.2.3、建设牢靠的音讯通道
JS 与 Flutter 的通信是 Flutter 动态化的基石,而首次通信的胜利与否又是通信是否胜利建设的首要条件。
因为所有的跨三端通信都是“半双工”的,而 JS 与 Flutter 的环境筹备又各自齐全独立,这也就导致如果任一方环境筹备实现前,另一方就发送了音讯,这就会呈现环境未实现的一方无奈接管到音讯的状况,从而影响前面所有的通信,导致通信中断或错乱。
为了解决这种状况,JS 与 Flutter 中采取了以下策略来保障首次通信的顺利执行(以下以 A / B 代指 JS 与 Flutter 中的任一方):
- A 环境筹备实现后会立刻向 B 发送告诉;
- 如果 B 已筹备好则会立刻回复一条告诉,A 收到回复告诉后标记单方环境已建设,可进行后续的通信;
- 如果 B 未筹备好,则 A 将不会收到任何回复,直到 B 筹备好,此时 A / B 身份调换,会从新回到步骤 1。
3.3、组件更新与事件传递
3.3.1、JS 事件触发与传递
在将 JS 中的事件函数转换为 id 后,这个 id 也会与节点所属页面名称、节点 id 一起被携带到 Flutter 中,最终这三个信息会被包装为一个 Flutter 中的事件函数。
当在 Flutter 中触发事件时,首先会触发这个函数,该函数会向 JS 发送一条携带了页面名称、节点 id、事件 id 以及事件参数的音讯。JS 接管到该音讯后,首先会依据页面名称与节点 id 查找到触发了事件的节点,接着通过事件 id 在节点事件池中查找到对应的事件,传入参数并执行该事件。
3.3.2、JS 组件更新
触发事件的目标大部分都是为了更新页面上的内容,在 JS 中,组件更新的根本单位是自定义组件。
当一个自定义组件触发 setState() 后,会将该组件推入更新队列中期待更新。在节点进入队列之前会进行去重,从队列中进入第一个组件开始后的 16ms,队列将执行更新操作。在这 16ms 内进入队列中的其余待更新组件将会一起触发更新。
在理论进行更新操作前,会先对队列中的元素进行父节点的去重,即:顺次获取所有待更新节点,同时向上获取该节点的父节点,如果其父节点存在于以后队列中,则从队列中移除该待更新节点,不存在则保留。这样做是因为只有队列中存在了父组件,则子组件就肯定会被更新;其目标是为了执行起码次数操作,但实现尽可能多组件的更新。
组件的更新借鉴了 React 的组件更新 diff 算法,然而因为引入了 Flutter StatefulWidget 和 StatelessWidget 的概念,因而相比 React 的 diff 算法,thresh.js 的 diff 算法是粗粒度的。
两者雷同的中央在于:都会对每一个节点进行比照,以保障每一个节点的状态都正确,最终被正确更新。
不同点在于:React 除了会对同类型节点进行属性和状态的合并外,也会将新创建或被删除的节点在旧节点数组中进行插入或删除操作,操作和更新的根本单位是原子组件;而 thresh.js 只会关注那些更新前后仍然保留的同类型节点,在实现属性与状态的合并后,会间接摈弃旧节点,保留新节点,最终新节点将替换待更新自定义组件中的旧节点,并应用更新后的自定义组件的数据向 Flutter 收回更新音讯——更新的根本单位是自定义组件。
3.3.3、Flutter 组件更新
JS 发送的更新音讯有两局部组成:须要被更新的页面名称、更新节点 id 以及更新节点的 JSON 数据。当 Flutter 收到 JS 发送的更新音讯后,首先会反复 json 转换为 Model 步骤,创立出 ModelTree & WidgetTree. 之后通过更新的页面名称和节点 id 在缓存中查找到须要被更新的 Model。
因为更新以 JS 中的自定义组件为最小单位,而每个自定义组件在 Flutter 中都会被创立为 StatefulWidget,因而在获取到新旧两个 Model 后会进行如下操作:
- 将 newModel 的渲染数据、子节点 models 及其所持有的 newWidget 合并到 oldModel;
- 通过 oldModel 所持有的 state 将 statefulWidget 中所包裹的 oldWidget 更新为 newWidget;
- 通过 state 实现组件更新操作后,Flutter 会对被更新的组件进行 diff 与从新渲染,以保障页面可能显示新的内容。
# 四、工程化
## 4.1、Thresh 架构
Thresh 的整体工程化架构如下图:
如上图所示,自下而上,CI/CD + 根底服务 + 监控上报等撑持了 Thresh 业务,最下面为架构图。
- X-RAY 为公司自研的生产公布平台,反对 Bundle 包的构建下发以及运维。
- 顶部是整体 Thresh 的架构流程图,蕴含 页面开发、DSL 的转换、通信等等,用于构建页面与逻辑。
Thresh 动态化跨平台计划尽管在设计上有高性能渲染、一致性、开发效率、前端同学零老本接入等劣势,然而思考将来多业务方接入以及晋升开发调试效率,推动了 Thresh 周边基础设施建设,上面简略介绍开发期、调试期、公布期。
- 开发期
反对 plugin 形式接入,业务方接入提供一套模板工程,能疾速进入业务开发;另外 Thresh 兼容 TS,能较低成本的让前端开发融入。 - 调试期
通过反对 HotReload 模式,秒级编译,极大的晋升开发和调试效率,另外提供调试面板 + 动静调试能力也能极大地辅助进步调试效率。 - 公布期
依赖满帮自研的 X -RAY 灰度公布零碎,具备分钟级别动静发版能力,能疾速撑持业务和问题修复。
4.3、Thresh 开发集成
Thresh 的开发集成造成了一整套流程,涵盖三方集成、多业务模块接入、开发调试等等,其中波及细节比拟多,这个在开源仓库外面有具体介绍。
至此,Thresh 的架构设计和开发集成能力都根本实现,相比于其余动态化跨平台开发框架,Thresh 有如下劣势:
- 基于 JS 的自定义 DSL,扩展性强,学习成本低
- 多端一致性,领有对立的自渲染引擎 skia,较好的跨端兼容性适配
- 反对 Hot Reload,便于开发调试,秒级编译
-
反对组件级别 UI 刷新,极佳的体验性
- 提供开发期调试面板,不便开发
五、结束语
通过 JS 构建 Flutter 应用程序的基本原理并不简单,次要是 JS 中的数据处理、Flutter 中的数据转换,以及实现数据在 JS 和 Flutter 中的流转通道。这类计划大提都相似,比方 MXFlutter、美团外卖 MTFlutter。不过,这种计划目前看来还是比拟鸡肋,偏离了 Flutter 跨平台波及的初衷。