前言
如今Flutter框架越来越趋于成熟,对于跨端渲染一致性问题有极致要求的项目可以更方便地迁移到Flutter框架上,而对于那些还在犹豫的项目组最大的阻碍可能就是动态化的解决方案上了。MXFlutter(https://juejin.im/post/5d11a4f06fb9a07ec63b21ea
)为我们提供了一个很好的思路,最近由于项目上的需求,我对该方案也做了一些调研,下面我们就从源码层面粗略地分析一下MXFlutter动态化方案的原理,希望能抛砖引玉引出更多优秀的点子。
Widget->Element->RenderObject
我们先来分析一下一个最普通的用dart编写的Flutter Widget是怎么工作的:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}
这是一个最简单的Flutter Hello World程序,我们先从main函数看起,里面调用了WidgetBinding的runApp方法,WidgetBinding是Widget框架和Flutter引擎的胶水层(The glue between the widgets layer and the Flutter engine.)。在runApp方法中调用attachRootWidget将我们定义的MyApp作为根布局添加到屏幕上:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
紧接着在attachRootWidget中又会通过RenderObjectToWidgetAdapter来最终把Element与RenderObject结合起来。通过官方注释我们也可以了解到RenderObjectToWidgetAdapter是Element与RenderObject之间的一座桥梁:
RenderObjectToWidgetAdapter: A bridge from a RenderObject to an Element tree.
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement);
}
我们接着看attachToRenderTree方法:
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
在这里就是Element被创建的地方,而这个createElement方法则是每个具体的Widget类自己实现的,以Flutter的StatelessWidget为例:
/// Creates a [StatelessElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatelessElement createElement() => StatelessElement(this);
我肯可以看到在createElement会把Widget的引用传给Element。在Element的preformBuild会调用到Element的build方法,而Element的build实际是调用了Widget的build:
/// Calls the [StatelessWidget.build] method of the [StatelessWidget] object
/// (for stateless widgets) or the [State.build] method of the [State] object
/// (for stateful widgets) and then updates the widget tree.
///
/// Called automatically during [mount] to generate the first build, and by
/// [rebuild] when the element needs updating.
@override
void performRebuild() {
···
Widget built;
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
···
} finally {
···
}
···
}
class StatelessElement extends ComponentElement {
@override
Widget build() => widget.build(this);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
···
}
}
而Widget的build方法,则是我们在开头看到的需要业务侧重写的具体的页面布局。这里有一个细节,Widget的build接收的参数是BuildContext,而我们传入的是Element,查看源码就可以发现,Element其实是继承了BuildContext。至此Widget->Element->RenderObject在代码层面的调用关系大概就理清楚了。
MXFlutter原理介绍
核心思路是把 Flutter 的渲染逻辑中的三棵树中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,可以使用 JavaScript,用极其类似 Dart 的开发方式,开发Flutter应用,利用JavaScript版的轻量级Flutter Runtime,生成UI描述,传递给Dart层的UI引擎,UI引擎把UI描述生产真正的 Flutter 控件。——MXFlutter
以MXFlutter Android端Demo为例,该项目集成了J2V8(https://eclipsesource.com/blogs/tutorials/getting-started-with-j2v8/
),这个库的作用是在没有WebView环境下,也能方便地进行JS与Java之间的相互通信。
在MXFlutter里通过用JS维护了一颗假的Widget树,然后通过JS-Java-Dart映射到Dart侧的Widget树。我们可以看到在Dart侧的MXJSWidget只是一个容器,内部封装了JS侧的MXJSWidget
@override
Widget build(BuildContext context) {
MXJSLog.log("MXJSWidget:build: ${widget.widgetData} ");
if (widget.widgetData == null) {
return _buildErrorWidget();
}
var w = _jsonBuildOwner.build(widget.widgetData, context);
//告诉JS层,使用当前JSWidget 序列号的数据构建,callbackID,widgetID 与之对应
_jsonBuildOwner.callJSOnBuildEnd();
return w;
}
在启动MXFlutter的Demo时,会先启动一个真实的Dart Widget——main.dart,在点击JSFlutter UI Demo后,会调用MXJSFlutter的navigatorPushWithPageName方法,通过内部一系列的方法调用最终返回一个MXJSWidget
//flutter层 主动push页面
MXJSWidget navigatorPushWithPageName(String widgetName, {ThemeData themeData, MediaQueryData mediaQueryData, IconThemeData iconThemeData}) {
MXJSWidget jsWidget = currentApp?.navigatorPushWithPageName(widgetName,
themeData: themeData, mediaQueryData: mediaQueryData, iconThemeData: iconThemeData);
return jsWidget;
}
//push js页面
//先创建一个空的MXJSWidget,调用JS,等待JS层widgetData来刷新页面
MXJSWidget navigatorPushWithPageName(String widgetName,
{ThemeData themeData, MediaQueryData mediaQueryData, IconThemeData iconThemeData}) {
···
callJSNavigatorPushWithPageName(widgetName,
themeData: themeData, mediaQueryData: mediaQueryData, iconThemeData: iconThemeData);
firstBuildWidget = jsWidget;
return jsWidget;
}
//flutter层 主动push页面
callJSNavigatorPushWithPageName(String widgetName,
{ThemeData themeData, MediaQueryData mediaQueryData, IconThemeData iconThemeData}) async {
MethodCall jsMethodCall =
MethodCall("flutterCallNavigatorPushWithPageName", {
"pageName": name,
"themeData": MXUtil.cThemeDataToJson(themeData),
"mediaQueryData": MXUtil.cMediaQueryDataToJson(mediaQueryData),
"iconThemeData": MXUtil.cIconThemeDataToJson(iconThemeData),
});
callJS(jsMethodCall);
}
void callJS(MethodCall jsMethodCall) {
MXJSLog.log("callJSWidget:$jsMethodCall");
var jsArgs = {
"method": jsMethodCall.method,
"arguments": jsMethodCall.arguments,
};
_jsFlutterAppChannel.invokeMethod("callJS", jsArgs);
}
等待JS侧执行完毕后会返回一个widgetData的json结构,dart侧则根据这个数据结构构建出相应的widget
//js->flutter 显示js页面
Future<dynamic> reloadApp(args) async {
String routeName = args["routeName"];
if (routeName == "MXJSWidget") {
String widgetDataStr = args["widgetData"];
var widgetData = json.decode(widgetDataStr);
try {
var w = currentApp.createJSWidget(widgetData);
currentApp.runJSApp(w);
} catch (e) {
MXJSLog.log("reloadApp error:$e");
throw (e);
}
} else {
//runApp(MyApp());
}
}
在createJSWidget方法中一路调用最终会走到MXJsonObjToDartObject的jsonObjToDartObject方法:
dynamic jsonObjToDartObject(MXJsonBuildOwner buildOwner, dynamic jsonObj, {BuildContext context}) {
String className;
try {
///map
if (jsonObj is Map) {
className = getJsonObjClassName(jsonObj);
///如果Map里找到了Class字段,则转换成对应Dart对象
if (className != null) {
return jsonMapObjToDartObject(buildOwner, jsonObj, context:context);
} else {
///如果Map里没找到Class字段,则转换成对应Dart里的Map对象,并对齐子元素,递归转换
return jsonMapObjToDartMapRecursive(buildOwner, jsonObj, context:context);
}
} else if (jsonObj is List) {
return jsonListObjToDartListRecursive(buildOwner, jsonObj, context:context);
} else {
return jsonObj;
}
} catch (e) {
MXJSLog.error(
"MXJsonObjToDartObject:jsonObjToDartObject error:$e ;decode:class $className, jsonObj:$jsonObj ");
//打印日志重新抛出
throw e;
}
}
后记
这套方案通过JSCore代替DartVM,使用JS来写Widget,从而实现了动态化。但至于MXFlutter所宣传单“高性能”,我觉得还是有待商榷,比起原生的Flutter,JS与Native、Dart间的通信增加了不小的额外开销。同时由于J2V8库的引入,对于包体积也会有不小的增加,但这的确是一条可行的动态化思路。后续如果能持续维护的话,增加对于Vue、RN的DSL转换也是能有很大的现实意义。