作者:SoapY
原文:https://juejin.im/post/5d11a4f06fb9a07ec63b21ea
基于JS的高性能Flutter动态化框架
这可能是目前放出来的相对最完整的Flutter动态化方案
缘起:18年10月份,我们团队的iOS产品尝试引入 Flutter,做为iOS开发,一接触到Flutter就马上感受到,Flutter 虽然强大,但不能动态化是阻碍我们使用她的唯一障碍了。舍弃Native的开发方式,一个很大的诉求是获取动态更新的能力。看Google团队对动态化的措辞,应该指望不上了,自己动手丰衣足食。
简介
项目代号:MXFlutter (Matrix Flutter)
核心思路是把 Flutter 的渲染逻辑中的三棵树中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,可以使用 JavaScript,用极其类似 Dart 的开发方式,开发Flutter应用,利用JavaScript版的轻量级Flutter Runtime,生成UI描述,传递给Dart层的UI引擎,UI引擎把UI描述生产真正的 Flutter 控件。所以,他在iOS上是完全动态化的 ,完整代码在github:
如果能帮助到大家,请给MXFlutter点个Star,给我们有动力继续更新下去^_*,也让整个Flutter社区都能了解到我们开发者的贡献。github TGIF-iMatrix MXFlutter(https://github.com/TGIF-iMatrix/MXFlutter)
继续前先瞥一眼整体的架构,一句话介绍MXFlutter,就是用JavaScript,以Flutter的写法开发Flutter。汗...还是有点绕,大家看下面贴出来的代码吧。
效果
先看看使用效果,以下截图是在MXFlutter框架下用JS开发,大家可以把上面的源码下载下来,里面有完整的JS代码示例:
这个是APP示例截图
下面是UI截图对应的JS代码,没错,你没有眼花,这个是真的 JavaScript 代码,可以在 MXFlutter 的运行时库上渲染出 Flutter 的UI
class JSPestoPage extends MXJSWidget {
constructor() {
super("JSPestoPage");
this.recipes = recipeList;
}
build(context) {
let statusBarHeight = 24;
let mq = MediaQuery.of(context);
if (mq) {
statusBarHeight = mq.padding.top
}
let w = new Scaffold({
appBar: new AppBar({
title: new Text("Pesto Demo")
}),
floatingActionButton: new FloatingActionButton({
child: new Icon(new IconData(0xe3c9)),
onPressed: this.createCallbackID(function () {
}),
}),
body: new CustomScrollView({
semanticChildCount: this.recipes.length,
slivers: [
//this.buildAppBar(context, statusBarHeight),
this.buildBody(context, statusBarHeight),
],
}),
//body:this.buildItems()[0]
});
return w;
}
buildAppBar(context, statusBarHeight) {
return SliverAppBar({
pinned: true,
expandedHeight: _kAppBarHeight,
actions: [
IconButton({
icon: new Icon(new IconData(1)),
tooltip: 'Search',
onPressed: this.createCallbackID(function () {
}),
}),
],
flexibleSpace: LayoutBuilder({
builder: function (context, constraints) {
size = constraints.biggest;
appBarHeight = size.height - statusBarHeight;
t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight);
extraPadding = new Tween({ begin: 10.0, end: 24.0 }).transform(t);
logoHeight = appBarHeight - 1.5 * extraPadding;
return Padding({
padding: EdgeInsets.only({
top: statusBarHeight + 0.5 * extraPadding,
bottom: extraPadding,
}),
child: Center({
child: new Icon(new IconData(1))
}),
});
},
}),
});
}
buildBody(context, statusBarHeight) {
let mediaPadding = EdgeInsets.all(0);
let mq = MediaQuery.of(context);
if (mq) {
mediaPadding = MediaQuery.of(context).padding;
}
let padding = EdgeInsets.only({
top: 8.0,
left: 8.0 + mediaPadding.left,
right: 8.0 + mediaPadding.right,
bottom: 8.0
});
return new SliverPadding({
padding: padding,
sliver: new SliverGrid({
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent({
maxCrossAxisExtent: _kRecipePageMaxWidth,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
}),
delegate: new SliverChildBuilderDelegate(
function (context, index) {
let recipe = this.recipes[index];
let w = new RecipeCard({
recipe: recipe,
onTap: function () { showRecipePage(context, recipe); },
});
return w;
},
{
childCount: this.recipes.length,
}),
}),
});
}
源码中还有更丰满的示例,高仿知乎页面JSFlutter版github.com/TGIF-iMatri…,这是对应UI,已经接近在线上版直接使用了。
现状
MXFlutter虽然各个模块已相对完整,但投入生产还需要解决其中的BUG,由于19年初,小组启动新项目,非常繁忙,几乎没有时间继续开发,从3月份一直暂停,目前人力仍然很紧张,如果大家有兴趣,期待小伙伴们一起加入,共同丰富 MXFlutter 动态化能力。
00 分享下动态化探索过程中的几个炮灰方案
Flutter 动态化方案一:静态解析Dart语言,生成UI描述
Dart 本身是描述语言,IDE 的 Outline 工具可以解析 Dart 代码生成树形结构,我们可以利用其源码,生成 JSON UI 描述,相关代码:github.com/flutter/flu…dart-sdk: analysis_server
静态解析 Dart 缺点,不能写逻辑,对编写UI代码有很多限制,不能写判断语句,不能写函数,要支持这些成本很高。所以只好放弃。
响应式UI框架
WidgetTree:Widget 里面存储了一个视图的配置信息,可以高效的创建(build)和销毁
Element 是分离 WidgetTree 和真正的渲染对象的中间层, WidgetTree 用来描述对应的Element 属性
RenderObject 来执行 Diff, Hit Test 布局、绘制
第一棵树有完整的UI描述信息,那么我只要JIT下通过 DartVM 创建第一棵树,其他耗时的操作都丢到AOT里去。
和方案一静态解析Dart对比,第二个方案是写一个极其轻量的运行时库,让编写UI的Dart 代码运行了起来,生成树形结构,再序列化为 JSON(debug),FlatBuffers (release)UI 描述。可以称之为动态解析方案
具体渲染逻辑
总体架构
架构也有了,方案也有了,要Run起来还有几个麻烦事要忙活,DartVM 要抽出来,Dart JIT层的轻量级运行时库,Dart AOT层把DSL转成真正Widget的UIEngin也要写哦,就是图中黄色和红色的三部分
抽离DartVM
Dart源代码在进行编译时会通过DART_PRECOMPILED_RUNTIME宏进行条件编译从而在Debug版编译JIT模式,Release版编译AOT模式。并且这两种模式是互斥的,无法同时存在。
我们单独编译出一个DartVM,打包成动态库,修改导出符号,避免符合冲突
开发DartVM与Native互通接口,参考了Flutter,使用Native Extension和Dart_Invoke实现互相调用
双DartVM调试方案,两个DartVM独立运行,通过远程端口单独调试DartFlutter
支持引入第三方库,DartFlutter在打包发布时会通过shell脚本分析.packages文件将依赖库自动打包随Dart File Zip一起随包下发。常用库可以预先打包的App本地,减少下发文件大小
安装包过大,DartVM增大安装包30M,如果加上原本的AOT40M,整个Flutter安装包会增大到70M,用DartVM不现实。怎么办呢。
01 最终方案 JavasSriptCore 替换 DartVM
可性能分析
JavasSriptCore 是iOS官方库,不增加安装包
Dart代码和JS代码非常相近,可以用工具转换
JavasSriptCore 与 Native有更方便的互调接口
ReactNative 已验证通过JS开发App能力是可行的
JS的执行效率是DartVM的3倍编码1M的JSON只需 2毫秒
用JS开发假的Flutter Runtime 封装JavasSriptCore与Native、 Flutter互调接口
02 讲解下 MXFlutter 的渲染原理
渲染树
两个重要的数据结构
MXScriptWidget
MXWidgetTree
MXScriptWidget管理一个Script页面或控件,负责创建管理 ScriptWidgetTree,以自增ID与Flutter对应Widget相互调用 ,每次Build都会创建一个新的MXWidgetTree
在 JS 侧 buildWidget 时,我们会对 function 事件,生成自增的唯一 callbackID,并与 widgetID 组合拼接成 widgetID/callbackID,作为事件的唯一标识。用户点击界面某个 button 时,事件由 Flutter 侧传到 JS 侧,通过解析 widgetID/callbackID,找到对应 widget 的 callback,完成事件处理。
通过在 JS 侧,ListView 调用 Build 方法时,提前展开 child, 并为 ListView 增加 children 成员变量。此时,因为仅有数据配置,不会有多余的 Layout 过程,所以速度是非常快的。
preBuild(jsWidget, buildContext) {
if(this.builder) {
for (let i = 0; i < this.childCount; ++i) {
let w = this.builder(buildContext, i);
this.children.push(w);
}
delete this.builder;
}
super.preBuild(jsWidget, buildContext);
}
在 Flutter 侧,ListView 仍然是动态创建,滑动列表,MXFlutter Engine 根据 Children 数组里的配置数据,创建真正的 Flutter WidgetCell,效率与原生相同完全一致。
ListView.builder(
itemCount: children.length,
itemBuilder: (context, index) {
return UIEngine.toWidget(children[index]);
},
)
动画参数在VM层配置一次,动画开始后在Flutter层闭环循环rebuild,形成动画效果,这个是比较通用的做法了。
03 渲染优化
不管JSWidget创建有多快,总是有跨语言执行,所以减少Build次数和减小Build出来的DSL UI描述大小,可以优化性能。
自动对比两次Widget 无论如何都没有直接创建一个新的快,如果开发者不参与,由框架来自动计算Diff是得不偿失的
牺牲响应式UI框架的设计模式 采用和Native、Web的方式,由开发者参与自己设置Diff的节点,即根据ID获取对应Widget,修改Widget参数,Rebuild生成新DSL
MXScriptWidget 是一个具备Build WidgetTree,缓存Callback映射表,动画支持的基本单位。可以作为普通FlutterWidget来使用。
在Flutter层,如果Widget树中节点有MXScriptWidget,则在对应节点上创建MXFlutterWidget自定义控件
两个子树可以相互对应获得局部刷新,callback回调,动画支持,Rebuild时所生产的UI DSL 很大减少,加快刷新速率
MXStatelessWidget 可以通过使用无状态的ScriptWidget来向框架标示,其下面的子树,在每次build中不会变化,其build结果会被缓存,下次在Flutter层直接复用
VM层,Flutter层,Native层镜像对象的生命周期如何控制?参考 iOS JavaScriptCore 和 Objective-C的解决方法
以Flutter层的对象生命周期为主
在VM层增加WeakMap支持,不增加对象引用计数,Flutter层释放之后,释放VM层对象
在Native层使用 JSManagerValue,VM层对象释放后,Native的引用被自动置空
参照业界RN等框架的设计,VM层跑在一个单独的后台线程
从Flutter层通过Native通道调用到VM,发生两次线程切换
Flutter UI层和MXScript层是异步调用,限制动态控件的架构设计
一个可行方案 修改FlutterEngine ,定制开发Dart->Native->VM 这个通道,调用到VM不切换线程 VM不新建线程,直接由Flutter UI Thread 消息循环驱动,这样也同时支持了和Flutter UI 层的高效同步调用,但要注意从Native调用到VM,需要通过定制FlutterEngine的接口。
04 让开发者写出优雅的代码
让开发者写出优雅的代码,咳咳,这里有点吹了,总之,我们想让使用MXFlutter的开发同学写出来的代码看来正规一些,好看一些。
完美支持Dart Flutter语法
定义所有Flutter 中同名Widget类,构建Widget的参数类,支持相同的Build方式,SetState触发刷新,事件响应函数
Callback函数自动生成CallbackID
Callback函数自动This绑定
ListView 像Dart层一样开发,支持itemBuilder回调函数
参考JS示例源码TGIF-iMatrix home_page.js(https://github.com/TGIF-iMatrix/MXFlutter/blob/master/js_flutter_src/app_test/home_page.js)
05 MXFlutter 基础建设
另外,我们通过重定向模拟器 JS 路径文件到开发机,用户修改完 JS 文件,便可直接看到相应修改,实现模拟器的页面热更新。
结语
要了解全部,一定要拉下源码,运行起来看看,有问题可以留言一讨论,MXFlutter会持续更新。
项目成员 luca 浪哥,nice,yockie 帅哥贡献了动画,控件,示例 APP 等核心实 现, chaodong 老师负责了 DartVM 方案,IP 老师帮忙提供了单元测试,健身大 神 yofer 老师负责了代码维护,工具建设。
TGIF-iMatrix 是一个技术氛围浓厚,有美女有帅哥有趣有爱的团队,还有精通量子计算,5G 等前沿技术的数据分析 victor 老王。