最右 JS2Flutter 框架——渲染机制(二)

用户头像
刘剑
关注
发布于: 2020 年 07 月 15 日
最右JS2Flutter框架——渲染机制(二)

1、概述

在上一篇文章最右JS2Flutter框架——开篇[1]中,我们已经介绍了如何实现最简单的Hello World,示例中只涉及到Text一个Widget,实际开发过程中,会需要用到各种丰富的Widget,甚至还有自定义的Widget。我们需要解决两个问题,怎么维护好这些Widget并渲染出页面,以及如何处理页面的刷新、跳转、退出等操作。



2、主流程

JS2Flutter框架在编译期间会把开发者对Flutter的依赖切换到镜像Flutter之上,并借助dart2js将Flutter镜像及业务代码编译成js,这也是实现对Flutter开发者透明的关键所在。在运行时跟Flutter的流程类似,通过runApp从根节点开始构建Client的虚拟树,Binding的过程中会依赖镜像的Widget、Animation、Gestures等,虚拟树构建完成之后,会序列化成JSON,传递到Host侧,然后通过ClassInstantiation解析并构建出真实的Widget树,然后绑定到AppContainer之上。当Host侧接收到事件之后,回溯给Client侧的镜像Widget,并回传给业务。借助下图更容易理解整个过程。



3、虚拟树构建

我们要先理解Flutter从Widget树构建出Element树的流程,不清楚的同学可以查看Gityuan的博客——深入理解Flutter应用启动[2]。Flutter为了满足不同的需求场景,提供了一些比较基础等Widget,比如StatelessWidget、StatefulWidget、LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget等。在理解了这个过程之后,我们确信需要跟Flutter一样,提供一些基础的Widget,这些基础的Widget都有与之对应的Element,跟Flutter的工作方式一样,在WidgetBinding的过程中,从根节点的Widget对应的Element开始mount,层层构建子Widget的Element并mount,直到把所有的节点都记录下来,从而完成Client侧虚拟树的构建。



他们都是Widget的派生类,而Widget本身继承自DartObject。DartObject是为了记录类的信息并提供数据化能力,当然它不只针对于Widget,它同样适用于Color、Offset等数据类。



abstract class DartObject {
const DartObject();

Map<String, dynamic> toJson() => {'className' : runtimeType.toString()};
}

每个Widget都会重写toJson,将自己的属性记录到节点信息中去,以MaterialButton为例,除了记录UI相关的属性外,如果有监听事件响应,也需记录下来。

class MaterialButton extends MapChildWidget {
const MaterialButton({
Key key,
this.onPressed,
this.onHighlightChanged,
this.textColor,
...
this.minWidth,
this.height,
this.child,
}) : super(key: key);

final VoidCallback onPressed;

final ValueChanged<bool> onHighlightChanged;

final Color textColor;

...

final double minWidth;

final double height;

@override
Map<String, Widget> children() {
return {'child': child};
}

@override
bool shouldGenerateId() {
return onPressed != null || onHighlightChanged != null;
}

@override
void handleEvent(String action, dynamic data, int callbackId) {
if (action == 'onPressed') {
onPressed();
} else if (action == 'onHighlightChanged') {
onHighlightChanged(data);
}
}

@override
Map<String, dynamic> toJson() {
Map<String, dynamic> json = super.toJson();
if (onPressed != null) {
json['onPressed'] = true;
}
if (onHighlightChanged != null) {
json['onHighlightChanged'] = true;
}
if (textColor != null) {
json['textColor'] = textColor.toJson();
}
...
if (minWidth != null) {
json['minWidth'] = minWidth.toString();
}
if (height != null) {
json['height'] = height.toString();
}
return json;
}
}



4、真实Widget树构建

虚拟树构建完成之后会数据化并传给Host侧,解析数据,还原出真实的Widget树。参考上一篇的Hello World例子,我们解析className,识别到是一个Text,从而构建出一个真实的Text并填充data。我们向树形结构拓展一下,从根节点开始,解析根节点的类型、属性、以及孩子节点,孩子节点也需要做一样的操作,直到它本身是一个叶子节点。理解这个过程之后,我们就要思考如何去实现,构造Widget的信息基本上都集中在构造函数中,如果Flutter能用反射,那就很简单了,只需要根据类名信息反射构造对应的Widget并填充属性,但是Flutter禁用了反射,所以我们只能提前把className和相应构造器的关系进行绑定,当需要构造某个Widget时,根据类名找到对应的构造器进行对应属性的解析,ClassInstantiation便是去完成这件事情。

typedef InstanceCreator = dynamic Function(Map<dynamic, dynamic> data);

class ClassInstantiation {
ClassInstantiation._() {
_instance = this;
}

static ClassInstantiation get instance => _instance;
static ClassInstantiation _instance;

Map<String, InstanceCreator> _instanceCreatorMap = new Map();

static void initForGlobal() {
ClassInstantiation classInstantiation = ClassInstantiation._();

registerWidgetsClass(classInstantiation);
registerMaterialClass(classInstantiation);
.
.
.
}

void register(String className, InstanceCreator creator) {
_instanceCreatorMap[className] = creator;
}

dynamic newInstance(String className, Map<dynamic, dynamic> data) {
InstanceCreator creator = _instanceCreatorMap[className];
if (creator == null) {
return null;
}

return creator(data);
}
}

dynamic newInstance(Map<dynamic, dynamic> data) {
if (data == null) {
return null;
}

String className = data['className'];
dynamic instance = ClassInstantiation.instance.newInstance(className, data);
if (instance == null) {
String classTag = data['classTag'];
instance = ClassInstantiation.instance.newInstance(classTag, data);
}

return instance;
}



事件应该如何处理呢?我们还是以MaterialButton为例,当它接收到onPressed事件之后,会传递给Client侧虚拟树中对应的镜像MaterialButton,再回调给业务。

MaterialButton materialButtonCreator(Map<dynamic, dynamic> data) {
int widgetId = data['widgetId'];
VoidCallback onPressed;
if (data['onPressed'] ?? false) {
onPressed = () {
Flutter2JSChannel.instance.sendWidgetEvent('onPressed', widgetId);
};
}

ValueChanged<bool> onHighlightChanged;
if (data['onHighlightChanged'] ?? false) {
onHighlightChanged = (value) {
Flutter2JSChannel.instance
.sendWidgetEvent('onHighlightChanged', widgetId, value);
};
}

return MaterialButton(
key: newInstance(data['key']),
child: newInstance(data['child']),
onPressed: onPressed,
onHighlightChanged: onHighlightChanged,
textColor: newInstance(data['textColor']),
...
minWidth: numToDouble(data['minWidth']),
height: numToDouble(data['height']),
);
}



5、状态的更新

页面的刷新、跳转、退出以及弹窗等都是状态的更新,不仅要更新虚拟树,而且要同步到Host侧,对真实的Widget树进行更新。



5.1 刷新

刷新主要是针对StatefulWidget,我们照样先看看Flutter是如何处理StatefulWidget的更新机制的,不清楚的同学可以查看Gityuan的博客——深入理解setState更新机制[3]。我们可以像Flutter一样,通过标记脏节点,重新构建子树的差量更新,当然也可以直接更新整个子树,对于Client侧来说,这两种方案差异不大,因为真实的渲染树是否更新并不由它们决定,而是通过差量数据构建出来的真实Widget子树与原来的真实Widget子树之间是否存在差异。



5.2 跳转和退出

跳转其实是分为两类的,一类是通过在WidgetsApp、MaterialApp、CupertinoApp提前注册的路由表去实现,这类属于静态注册,另一类是通过动态构建Route去实现,属于动态注册。弹窗就是一种动态注册。



最右是通过预占坑的方式去实现页面的跳转的,静态注册的路由会在WidgetsApp、MaterialApp和CupertinoApp的构造器中去还原路由表,每个路由都会构造一个预占坑的壳与之对应,动态注册的页面通过Navigator.of(context).push实时构建坑位,这些坑位都是StatefulWidget,当坑位initState的时候,向Client侧索取页面的虚拟树,拿到虚拟树之后构建出真实的Widget树,Client侧在构建出虚拟树之后会直接挂载到WidgetsApp、MaterialApp、CupertinoApp下面,当页面退出时,坑位触发dispose,此时请求Client侧的虚拟树移除对应子树。



5.3 AppLifecycleState

其实我们还涉及到App状态的监听,很多时候我们需要感知App的生命周期,来完成一些事情。Flutter通过WidgetsBinding提供了此能力,最右是如何解决这个问题的呢?借助AppContainer。细心的同学可能会发现深入理解Flutter应用启动[2]文章中提到了起点runApp(Widget app),我们是从数据流向的过程逐步剖析JS2Flutter的渲染过程,但实际上在Host端,Engine启动之后,会runApp一个AppContainer,它也是一个预占坑的StatefulWidget,当真实Widget树构建出来之后会刷新AppContainer,这是一个从始至终都存在的Widget,它的生命周期等于整个Flutter App的生命周期。我们可以借助它监听AppLifecycleState的变化,然后传递给Client侧的WidgetsBinding,Client侧的WidgetsBinding会去管理注册、分发等。



6、Canvas绘制

除了通过上述的Widget去描述UI之外,还有一类特殊的渲染方式,比如CustomPainter,需要通过Canvas绘制。这部分会在【最右JS2Flutter框架——动画、小游戏的实现】一文中进行详解。



7、结束语

本文阐述了最右JS2Flutter框架的渲染机制,实际上还有很多细节问题并未继续展开,感兴趣的同学可以发散思考,欢迎留言探讨。



8、参考文献

[1]:最右JS2Flutter框架——开篇 https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1

[2]:深入理解Flutter应用启动 http://gityuan.com/2019/06/29/flutter_run_app/

[3]:深入理解setState更新机制 http://gityuan.com/2019/07/06/flutter_set_state/



用户头像

刘剑

关注

还未添加个人签名 2020.06.28 加入

最右 App Android 工程师,自2019年初加入最右,主要从事 Flutter 相关领域的技术探索,负责 Flutter 的动态化在最右 App 落地及成功实践。

评论 (1 条评论)

发布
用户头像
赞👍
2020 年 08 月 05 日 22:43
回复
没有更多了
最右JS2Flutter框架——渲染机制(二)