阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

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

  • 2020-01-07
  • 本文字数:5404 字

    阅读完需:约 18 分钟

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

1、写在前面

Flutter⾃诞生便备受关注,其高效的自渲染技术注定要在性能和体验上优于在这之前的跨端方案,美中不足的是目前 Flutter不具备像 Hybrid、RN、Weex 等拥有的动态更新能力,官⽅在 2019年的 Roadmap里面原本有支持动态化的想法,但后来又出于性能、安全等⽅面的考量而放弃了。


最右去做 Flutter 动态化的目的是为技术选型提供多样的选择,在一些使用 H5 但交互性强的场景,或者使用原生但非核⼼的独立场景,提供更优的方案。今天主要跟大家分享最右 App 在实现 Flutter 动态化的过程中的一些经验教训。 下⾯列出本篇文章的大纲和思路,以便于大家更好地理解:


2、先说 Android

2.1 实现思路

实现动态化,就是实现指定路径的代码和资源的加载,并确保代码能正常执行。


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



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

2.2 实现原理

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



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

2.3 工作流程

2.3.1 编译阶段

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

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

下⾯面是整个框架的结构图:



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 事件被触发。


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 文件中去。


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 工作流程


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-development-environment


[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


作者介绍: 刘剑,最右 App Android 工程师,近 1 年主要从事 Flutter 相关领域的技术探索,负责 Flutter 的动态化在最右 App 落地及成功实践。


2020-01-07 10:316158

评论 3 条评论

发布
用户头像
有相应的Demo参考下吗?谢谢。
2020-07-30 10:39
回复
用户头像
有相应的Demo参考下吗?谢谢。
2020-05-19 16:20
回复
用户头像
可以,写得很好👌
2020-03-11 22:43
回复
没有更多了
发现更多内容

银行APP用户体验外滩峰会圆满落幕!易观分析赋能用户体验体系,助力体验提升

易观分析

App 银行 易观

谁能破解客户数字化困局?

ToB行业头条

案例 | 九科信息助力某大型证券公司业务部数智化转型

九科Ninetech

【iOS逆向与安全】frida-trace入门

小陈

移动端 iOS逆向 ios安全

FAQ是什么?该如何编辑FAQ?

Baklib

十分钟带你全面解析Promise、generator、async类同步编程!

好程序员IT教育

JavaScript Promise

快围观!助力 TDesign 无障碍改造活动来了!

TDesign

无障碍

主成分分析PCA与奇异值分解SVD-PCA中的SVD

烧灯续昼2002

Python 机器学习 算法 sklearn 11月月更

QUIC学习入门概念及资料整理

黄继承

QUIC

芯启源加入龙蜥社区,推动集成电路和DPU芯片创新落地

OpenAnolis小助手

开源 龙蜥社区 CLA 芯启源

链路状态路由协议 OSPF (三)

我叫于豆豆吖.

11月月更

pytorch实现卷积神经网络实验

Studying_swz

人工智能 11月月更

如何做好成熟完整的企业团队知识管理?

Baklib

团队管理 知识管理

磁盘占用高问题如何排查?三步教你搞定

OceanBase 数据库

深度学习 | 如何开发、部署 Serverless 应用?

阿里巴巴云原生

阿里云 Serverless 云原生

埃森哲发布《2022中国企业数字化转型指数》,如何通过自动化工具打造技术底座

SoFlu软件机器人

无需重启应用,动态采集任意点位日志

阿里巴巴云原生

阿里云 云原生

综合实验——高级网络应用检测

我叫于豆豆吖.

11月月更

【C语言】if 关键字

謓泽

11月月更

Apisix网关快速入门实践

IT巅峰技术

调用链路上千条,如何观测 Nacos 的运行状态

阿里巴巴云原生

阿里云 微服务 云原生 naocs

2022年10月视频用户洞察:卡塔尔世界杯揭幕,全民体育盛宴开启

易观分析

视频 世界杯

Redis 持久化机制演进与百度智能云的实践

Baidu AICLOUD

数据库 Redis内核

Linux之用户管理、权限管理、程序安装卸载

C++后台开发

Linux 后端开发 linux开发 Linux服务器开发 C++开发

Baklib知识分享 | 搭建企业在线帮助中心的最佳攻略

Baklib

如何通过 NFTScan 浏览器捕获 NFT 投资机会

NFT Research

区块链 NFT 数据基础设施

PHP反序列化漏洞解析

网络安全学海

网络安全 安全 信息安全 渗透测试 漏洞挖掘

5分钟实现「视频检索」:基于内容理解,无需任何标签

Zilliz

人工智能 Towhee 视频检索

华为云发布1+3+M+N全球云基础设施布局 全面推动汽车产业数智升级

科技热闻

链路状态路由协议 OSPF (二)

我叫于豆豆吖.

11月月更

面了个阿里拿38k出来的,让我见识到了基础顶端

程序知音

Java java面试 java架构 后端技术 Java面试八股文

Flutter动态化在最右App中的实践_文化 & 方法_刘剑_InfoQ精选文章