简介:2018年10月,手Q看店团队尝试使用Flutter作为iOS开发。 他们一接触Flutter,立刻就觉得Flutter虽然强大,但它不能像RN那样动态,这是阻碍我们使用她的唯一障碍。 。 看Google团队的动态计划,短期内应该不会推出,所以我就自己开始了这个技术探索项目。
基于JS的高性能Flutter动态框架
可能是目前为止放下的相对最完整的Flutter动态解决方案
介绍
项目代号:MXFlutter(Matrix Flutter)
核心思想是将Flutter渲染逻辑中的三棵树中的第一棵放入JavaScript中生成。 Flutter控制层封装完全用JavaScript实现。 您可以使用JavaScript来开发Flutter应用程序,开发方式与Dart非常相似。 使用轻量级Flutter Runtime的JavaScript版本生成UI描述并将其传递给Dart层的UI引擎。 UI引擎 描述UI以产生真正的Flutter控件。 因此,它在iOS上是完全动态的,完整的代码在github中。 如果对您有帮助,请给MXFlutter一个Star,给我们继续更新的动力^_*,github TGIF-iMatrix MXFlutter
在继续之前,我们先看一下整体结构,一句话介绍一下MXFlutter,就是用JavaScript按照Flutter的方式来开发Flutter。 汗...还是有点混乱,我们看一下下面贴出的代码。
影响
以下截图是在MXFlutter框架下用JS开发的。 你可以下载里面的源码,里面有完整的JS代码示例:
这是APP的示例截图
下面是UI截图对应的JS代码。 是的,你没有眼花缭乱。 这是真正的 JavaScript 代码,可以在 MXFlutter 运行时库上渲染 Flutter UI
<pre class="code-snippet__js" data-lang="javascript">
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版本
。
这是对应的UI,已经接近于在线版中直接使用了。
这个漂亮的知乎页面是用Dart版本的JS转换而来的。 感谢作者徐继友。 你可以关注他。
现状
尽管MXFlutter的各个模块都比较完整,但在投入生产时仍然需要解决其中的Bug。 由于团队在2019年初启动了一个新项目,所以非常忙碌,几乎没有时间继续开发。 自3月份以来,该项目已暂停。 目前,人手非常紧张。 如果你有兴趣的话elementui验证框架,我期待和你一起丰富MXFlutter的动态能力。
0x00 分享动态探索过程中的几个炮灰解决方案
Flutter动态方案一:静态解析Dart语言并生成UI描述
Dart 本身是一种描述语言。 IDE的Outline工具可以解析Dart代码以生成树结构。 我们可以使用它的源代码来生成 JSON UI 描述。 相关代码:
dart-sdk:分析服务器
Dart静态分析的缺点是不能写逻辑,对UI代码的编译有很多限制。 不会写判断语句和函数。 支持这一点的成本非常高。 所以我不得不放弃。
快速介绍Flutter核心渲染模块的三棵树
响应式 UI 框架
WidgetTree:Widget存储了一个视图的配置信息elementui验证框架,可以高效地创建(构建)和销毁
Element是一个中间层,将WidgetTree与真正的渲染对象分开。 WidgetTree用于描述对应的Element属性
RenderObject 执行 Diff、Hit Test 布局、绘图
第一棵树有完整的UI描述信息,所以我只需要在JIT下通过DartVM创建第一棵树,随着时间的推移其他操作都扔到AOT中。
Flutter动态方案二:动态运行Dart语言并产生UI描述
与第一种静态分析Dart相比,第二种方案是编写一个非常轻量级的运行时库,让编译UI的Dart代码运行起来,生成树形结构,然后序列化为JSON(调试)、FlatBuffers(发布) UI描述.动态解析方案
具体渲染逻辑
整体结构
还有一个框架和解决方案。 运行它还有一些麻烦的事情要做。 需要移除 DartVM、Dart JIT 层的轻量级运行时库以及将 DSL 转换为 Dart AOT 层真正的 Widget 的 UIEngine。 哦,就是图中蓝色和黄色的三个部分
提取 DartVM
不能简单地改变编译条件来提取
Dart源码在编译时,会通过DART_PRECOMPILED_RUNTIME宏进行条件编译,所以在Debug版本中编译的是JIT模式,在Release版本中编译的是AOT模式。 并且这两种模式是互斥的,不能同时存在。
简单的解决方案是
我们单独编译一个DartVM,打包成动态库,并修改导入符号以避免合规性冲突
引入 DartVM 仍需开展工作
暂时未解决的问题
安装包太大,DartVM将安装包缩小了30M。 如果加上原来的AOT40M,整个Flutter安装包就会缩小到70M,这对于使用DartVM来说是不现实的。 怎么做。
0x01 最终方案JavasSriptCore替代DartVM
可能性剖析
JavasSriptCore是iOS官方库,不减安装包
Dart代码与JS代码非常相似,可以使用工具进行转换
JavasScriptCore和Native有更方便的互调套接字
ReactNative已经验证通过JS开发App能力是可行的
JS执行效率是DartVM的3倍,编码1M JSON仅需2毫秒
需要解决的问题
用 JS 开发一个假的 Flutter Runtime
封装JavasScriptCore和Native、Flutter互调socket
0x02 讲解MXFlutter的渲染原理
渲染树
两个重要的数据结构
MXScriptWidget管理一个Script页面或控件,负责创建和管理ScriptWidgetTree,通过自增ID与Flutter对应的Widget互相调用,每次Build时都会创建一个新的MXWidgetTree
MXFlutter 事件
在JS端构建Widget时,我们会为函数事件生成一个唯一的自增callbackID,并与widgetID组合起来形成widgetID/callbackID,作为storm的唯一标识。 当用户点击界面上的按钮时,事件从Flutter端传输到JS端,通过解析widgetID/callbackID找到widget对应的回调,完成风暴处理。
MXFlutter高效动态列表
在JS端,当ListView调用Build方法时,会提前展开child,并将children成员变量添加到ListView中。 这时,因为只有数据配置,不会有多余的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]);
},
)
MXFlutter动画方案
动画参数在VM层配置一次,动画开始后在Flutter层闭环重建,形成动画效果。 这是一种比较常见的做法。
0x03 渲染优化
无论 JSWidget 创建得有多快,总会存在跨语言执行的情况,因此减少 Build 次数并减小 Build 后 DSL UI 描述的大小可以优化性能。
渲染优化1-部分刷新:配置树差异
事实
无论如何,自动比较两个 Widget 并不会直接创建新的 Widget。 如果开发者不参与,框架自动估计Diff就得不偿失了
可行的办法
以牺牲响应式 UI 框架为代价的设计模式
Native和Web的形式,开发者自己参与设置Diff节点,即根据ID获取对应的Widget,修改Widget参数,重新构建生成新的DSL
渲染优化2-部分刷新-嵌套节点
渲染优化3-动静态控制可分离
MXStatelessWidget 可以通过使用无状态 ScriptWidget 被框架识别。 它下面的子树在每次构建中都不会改变。 构建结果会被缓存,下次直接在Flutter层复用。
内存-跨层镜像对象的生命周期
VM层、Flutter层、Native层如何控制镜像对象的生命周期?
参考Apple的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套接字。
0x04 让开发者写出高贵的代码
让开发者写出高贵的代码,咳咳,这有点吹了,总之我们想让使用MXFlutter的开发者写的代码看起来更正式、更好看。
参考JS示例源码
TGIF-iMatrix home_page.js
0x05 MXFlutter基础设施建设
由于JavaScript不支持模块化开发,无法引用其他文件代码,因此我们参考RN,使用Node.js模块化代码,并在Native层支持require语法。 开发时IDE最好选择VSCode,因为可以安装JS插件,直接运行调试JS
另外,我们将模拟器的JS路径文件重定向到开发机,用户更改JS文件后可以直接看到相应的变化,实现模拟器页面的热更新。
结语
由于时间限制,MXFlutter 还存在很多遗留问题。 作为一种技术探索,非常辛苦,但又非常有趣。 期待各位专家的指导,也期待朋友们提出的问题一起讨论解决。
要了解一切,您必须下载源代码,运行它并查看。 如果有什么疑问,可以留言讨论。 MXFlutter将持续更新。
其他项目成员包括 Luca Lang、nice 和 yockie,他们贡献了动画、控件和示例 APP 等核心实现。 Chaodong先生负责DartVM解决方案,IP先生帮助提供单元测试,健身专家Yofer先生负责代码维护和工具构建。 。
TGIF-iMatrix是一支技术氛围浓厚的团队,有帅哥有美女,有趣有爱,还有精通量子估计、5G等前沿技术的数据分析胜利者老王。 欢迎iOS、Android开发伙伴、数据开发、数据分析职位的朋友联系我发简历
对MXFlutter感兴趣的朋友可以进群交流
群号:747535761
另外,为我们的项目做一个小广告。 大家点点看一些视频——年轻人爱看的腾讯短视频。看视频