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

1,184 阅读7分钟

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框架——开篇 xie.infoq.cn/article/ace…

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

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