在 「Fan 直播」的 Flutter 混合开发实践中,咱们总结了一些 Flutter 混合开发的经验。本文第一篇章将从 Flutter 原理出发,详细介绍 Flutter 的绘制原理,借由此在第二章来对比三种跨端方案;以后再进入第三篇章 Flutter 混合开发模式的讲解,主要是四种不一样的 Flutter 混合模式的原理分析;最后的第四篇章,简单分享一下混合工程的工程化探索。html
“惟有深刻,方能浅出”,对于一门技术,只有了解的深刻,才能用最浅显、通俗的话语描述出。在此以前,我写过一些 Flutter 的文章,但性质更偏向于学习笔记与源码阅读笔记,所以较为晦涩,且零碎繁乱。本文做为阶段性的总结,我尽量以浅显易懂的文字、按部就班地来分享 Flutter 混合开发的知识,对于关键内容会辅以源码或源码中的关键函数来解读,但不会成段粘贴源码。源码学习的效果主要在于自身,因此若对源码学习感兴趣的,能够自行阅读 Framework 与 Engine 的源码,也能够阅读我过往的几篇文章。前端
好了,那废话很少说,直接开始吧!android
注:此图引自 Flutter System Overviewios
传统惯例,只要说到 Flutter 原理的文章,在开头都会摆上这张图。不论讲的好很差,都是先摆出来,而后大部分仍是靠自行领悟。由于这张图实在太好用了。c++
摆出这张图,仍是简单从总体上来先认识了一下什么是 Flutter,不然容易陷入“盲人摸象”的境地。git
Flutter 架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework。github
至于更多详情,这张图配合源码食用体验会更好。但因为本文不是源码解析,因此这个工做本文就不展开了。接下来,我会以 Flutter 绘制流程为例,来说解 Flutter 是如何工做的。这也能更好地帮助你理解源码的思路。算法
Flutter 绘制流程总结了一下大致上以下图所示:编程
首先是用户操做,触发 Widget Tree 的更新,而后构建 Element Tree,计算重绘区后将信息同步给 RenderObject Tree,以后实现组件布局、组件绘制、图层合成、引擎渲染。canvas
做为前置知识,咱们先来看看渲染过程当中涉及到的数据结构,再来具体剖析渲染的各个具体环节。
渲染过程当中涉及到的关键的数据结构包括三棵树和一个图层,其中 RenderObject 持有了 Layer,咱们重点先看一下三棵树之间的关系。
举个栗子,好比有这么一个简单的布局:
那么对应的三棵树之间的关系以下图所示:
第一棵树,是 Widget Tree。它是控件实现的基本逻辑单位,是用户对界面 UI 的描述方式。
须要注意的是,Widget 是不可变的(immutable),当视图配置信息发生变化时,Flutter 会重建 Widget 来进行更新,以数据驱动 UI 的方式构建简单高效。
那为何将 Widget Tree 设计为 immutable?Flutter 界面开发是一种响应式编程,主张“simple is fast”,而由上到下从新建立 Widget Tree 来进行刷新,这种思路比较简单,不用额外关系数据更变了会影响到哪些节点。另外,Widget 只是一个配置是数据结构,建立是轻量的,销毁也是作过优化的,不用担忧整棵树从新构建带来的性能问题。
第二棵树,Element Tree。它是 Widget 的实例化对象(以下图,Widget 提供了 createElement
工厂方法来建立 Element),持久存在于运行时的 Dart 上下文之中。它承载了构建的上下文数据,是链接结构化的配置信息到最终完成渲染的桥梁。
之因此让它持久地存在于 Dart 上下文中而不是像 Widget 从新构建,**由于 Element Tree 的从新建立和从新渲染的开销会很是大,**因此 Element Tree 到 RenderObject Tree 也有一个 Diff 环节,来计算最小重绘区域。
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
final Key key;
@protected
@factory
Element createElement();
/// ... 省略其余代码
}
复制代码
须要注意的是,Element 同时持有 Widget 和 RenderObject,但不管是 Widget 仍是 Element,其实都不负责最后的渲染,它们只是“发号施令”,真正对配置信息进行渲染的是 RenderObject。
第三棵树,RenderObject Tree,即渲染对象树。RenderObject 由 Element 建立并关联到 Element.renderObject
上(以下图),它接受 Element 的信息同步,一样的,它也是持久地存在 Dart Runtime 的上下文中,是主要负责实现视图渲染的对象。
RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element is RenderObjectElement)
result = element.renderObject;
else
element.visitChildren(visit);
}
visit(this);
return result;
}
复制代码
RenderObject Tree 在 Flutter 的展现过程分为四个阶段:
其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,肯定树中各个对象的位置和尺寸,并把它们绘制到不一样的图层上。绘制完毕后,合成和渲染的工做则交给 Skia 处理。
那么问题来了,为何是三棵树而不是两棵?为何须要中间的 Element Tree,由 Widget Tree 直接构建 RenderObject Tree 不能够吗?
理论上能够,但实际不可行。由于若是直接构建 RenderObject Tree 会极大地增长渲染带来的性能损耗。由于 Widget Tree 是不可变的,但 Element 倒是可变的。**实际上,Element 这一层将 Widget 树的变化作了抽象(相似 React / Vue 的 VDOM Diff),只将真正须要修改的部分同步到 RenderObject Tree 中,由此最大程度去下降重绘区域,提升渲染效率。**能够发现,Flutter 的思想很大程度上是借鉴了前端响应式框架 React / Vue。
此外,再扩展补充一下 VDOM。咱们知道,Virtual DOM 的几个优点是:
最后,看看 Layer,它依附于 RenderObject(经过 RenderObject.layer
获取),是绘图操做的载体,也能够缓存绘图操做的结果。Flutter 分别在不用的图层上绘图,而后将这些缓存了绘图结果的图层按照规则进行叠加,获得最终的渲染结果,也就是咱们所说的图像。
/// src/rendering/layer.dart
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// ... 省略无关代码
bool get alwaysNeedsAddToScene => false;
bool _needsAddToScene = true;
void markNeedsAddToScene() {
_needsAddToScene = true;
}
bool _subtreeNeedsAddToScene;
void updateSubtreeNeedsAddToScene() {
_subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
}
}
复制代码
如上图代码所示,Layer 的基类上有两个属性 _needsAddToScene
和 _subtreeNeedsAddToScene
,前者表示须要加入场景,后者表示子树须要加入场景。一般,只有状态发生了更新,才须要加入到场景,因此这两个属性又能够直观理解为「本身须要更新」和「子树须要更新」。
Layer 提供了 markNeedsAddToScene()
来把本身标记为「须要更新」。派生类在本身状态发生变化时调用此方法把本身标记为「须要更新」,好比 ContainerLayer 的子节点增删、OpacityLayer 的透明度发生变化、PictureLayer 的 picture 发生变化等等。
绘制流程分为如下六个阶段:
抛开 Diff 和 Render 咱们本文不讲解,由于这两部分稍稍繁琐一些,咱们来关注下剩下的四个环节。
注:此流程图出自 复杂业务如何保证Flutter的高性能高流畅度?| 闲鱼技术,能够较为清晰的表达 Flutter 核心的绘制流程了。
执行 build 方法时,根据组件的类型,存在两种不一样的逻辑。
咱们知道,Flutter 内的 Widget 能够分为 StatelessWidget 与 StatefulWidget,即无状态组件与有状态组件。
所谓 StatelessWidget,就是它 build 的信息彻底由配置参数(入参)组成,换句话说,它们一旦建立成功就再也不关心、也不响应任何数据变化进行重绘。
所谓 StatefulWidget,除了父组件初始化时传入的静态配置以外,还要处理用户的交互与内部数据变化(如网络数据回包)并体如今 UI 上,这类组件就须要以 State 类打来 Widget 构建的设计方式来实现。它由 State 的 build 方法构建 UI, 最终调用 buildScope
方法。其会遍历 _dirtyElements
,对其调用 rebuild/build。
注:以上两图出自 《Flutter 核心技术与实战 | 陈航》
只有布局类 Widget 会触发 layout(如 Container、Padding、Align 等)。
每一个 RenderObject 节点须要作两件事:
/// 实际计算 layout 的实现
void performLayout() {
_size = configuration.size;
if (child != null) {
child.layout(BoxConstraints.tight(_size));
}
}
void layout(Constraints constraints, { bool parentUsesSize = false }) {
/// ...省略无关逻辑
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
performResize();
}
RenderObject debugPreviousActiveLayout;
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
复制代码
如此递归一轮,每一个节点都受到父节点的约束并计算出本身的 size,而后父节点就能够按照本身的逻辑决定各个子节点的位置,从而完成整个 Layout 环节。
渲染管道中首先找出须要重绘的 RenderObject,若是有实现了 CustomPainter 则调用 CustomPainter paint 方法 再调用 child 的 paint 方法;若是未实现 CustomPainter,则直接调用 child 的 paint。
在调用 paint 的时候,通过一串的转换后,layer->PaintingContext->Canvas
,最终 paint 就是描绘在 Canvas 上。
void paint(PaintingContext context, Offset offset) {
if (_painter != null) {
// 只有持有 CustomPainter 状况下,才继续往下调用自定义的 CustomPainter 的 paint 方法,把 canvas 传过去
_paintWithPainter(context.canvas, offset, _painter);
_setRasterCacheHints(context);
}
super.paint(context, offset); //调用父类的paint的方法
if (_foregroundPainter != null) {
_paintWithPainter(context.canvas, offset, _foregroundPainter);
_setRasterCacheHints(context);
}
}
// 在父类的 paint 里面继续调用 child 的 paint,实现父子遍历
void paint(PaintingContext context, Offset offset) {
if (child != null){
context.paintChild(child, offset);
}
void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
int debugPreviousCanvasSaveCount;
canvas.save();
if (offset != Offset.zero)
canvas.translate(offset.dx, offset.dy);
// 在调用 paint 的时候,通过一串的转换后,layer->PaintingContext->Canvas,最终 paint 就是描绘在 Canvas 上
painter.paint(canvas, size);
/// ...
canvas.restore();
}
复制代码
合成主要作三件事情:
ui.window.render
方法,把 Scene 提交给 Engine。final ui.Window _window;
void compositeFrame() {
// 省略计时逻辑
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);
scene.dispose();
}
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
addChildrenToScene(builder);
}
void addChildrenToScene(ui.SceneBuilder builder, [ Offset childOffset = Offset.zero ]) {
Layer child = firstChild;
while (child != null) {
if (childOffset == Offset.zero) {
child._addToSceneWithRetainedRendering(builder);
} else {
child.addToScene(builder, childOffset);
}
child = child.nextSibling;
}
}
复制代码
跨端开发是必然趋势,从本质上来讲,它增长业务代码的复用率,减小由于适配不一样平台带来的工做量,从而下降开发成本。在各平台差别抹平以前,要想“多快好省”地开发出各端体验接近一致的程序,那即是跨端开发了。
总得来讲,业内广泛认同跨端方案存在如下三种:
下面来一一讲解。
所谓 Web 容器,便是基于 Web 相关技术经过浏览器组件来实现界面和功能,包括咱们一般意义上说的基于 WebView 的 “H5”、Cordova、Ionic、微信小程序。
这类 Hybrid 开发模式,只须要将开发一次 Web,就能够同时在多个系统的浏览器组件中运行,保持基本一致的体验,是迄今为止热度很高的跨端开发模式。而 Web 与 原生系统之间的通讯,则经过 JSBridge 来完成,原生系统经过 JSBridge 接口暴露能力给 Web 调用。而页面的呈现,则由浏览器组件按照标准的浏览器渲染流程自行将 Web 加载、解析、渲染。
这类方案的优势:简单、自然支持热更新、生态繁荣、兼容性强、开发体验友好。
固然,缺点也很明显,不然就没有后面两个方案什么事了,主要是体验上的问题:
因此轮到泛 Web 容器方案出场了,表明性框架是 React Native,Weex,Hippy。
在跨端通讯上,React Native 依然经过 Bridge 的方式来调用原生提供的方法。
这套方案理想是美好的,但现实确实骨感的,它在实践下来以后也依然发现了问题:
那咱们究竟能不能既简单地抹平差别,又同时保证性能呢?
答案是能够,那就是自绘引擎。不调用原生控件,咱们本身去画。那就是 Flutter。比如警察问 React Native 嫌疑犯长什么样子,React Native 只能绘声绘色地去描绘嫌疑犯的外观,警察画完以后再拿给 React Native 看,React Native 还要回答像不像;但 Flutter 本身就是一个素描大师,它能够本身将嫌疑犯的画像画好而后交给警察看。这二者的效率和表现差别,不言而喻。
经过这样的思路,Flutter 能够尽量地减小不一样平台之间的差别, 同时保持和原生开发同样的高性能。而且对于系统能力,能够经过开发 Plugin 来支持 Flutter 项目间的复用。因此说,Flutter 成了三类跨端方案中最灵活的那个,也成了目前业内受到关注的框架。
至于通讯效率,Fluter 跨端的通讯效率也是高出 JSBridge 许许多多。Flutter 经过 Channel 进行通讯,其中:
其中,MethodChannel 在开发中用的比较多,下图是一个标准的 MethodChannel 的调用原理图:
但为何咱们说 Channel 的性能高呢?梳理一下 MethodChannel 调用时的调用栈,以下图所示:
能够发现,整个流程中都是机器码的传递,而 JNI 的通讯又和 JavaVM 内部通讯效率同样,整个流程通讯的流程至关于原生端的内部通讯。可是也存在瓶颈。咱们能够发现,methodCall 须要编解码,其实主要的消耗都在编解码上了,所以,MethodChannel 并不适合传递大规模的数据。
好比咱们想调用摄像头来拍照或录视频,但在拍照和录视频的过程当中咱们须要将预览画面显示到咱们的 Flutter UI中,若是咱们要用 MethodChannel 来实现这个功能,就须要将摄像头采集的每一帧图片都要从原生传递到 Dart 侧中,这样作代价将会很是大,由于将图像或视频数据经过消息通道实时传输必然会引发内存和 CPU 的巨大消耗。为此,Flutter 提供了一种基于 Texture 的图片数据共享机制。
Texture 和 PlatformView 不在本文的探讨范围内,这里就再也不深刻展开了,有兴趣的读者能够自行查阅相关资料做为扩展知识了解。
那接下来,咱们就进入本文的第三篇章吧,Flutter 混合开发模式的探索。
Flutter 混合工程的结构,主要存在如下两种模式:
所谓统一管理模式,就是一个标准的 Flutter Application 工程,而其中 Flutter 的产物工程目录(ios/
和 android/
)是能够进行原生混编的工程,如 React Native 进行混合开发那般,在工程项目中进行混合开发就好。可是这样的缺点是当原生项目业务庞大起来时,Flutter 工程对于原生工程的耦合就会很是严重,当工程进行升级时会比较麻烦。所以这种混合模式只适用于 Flutter 业务主导、原生功能为辅的项目。但早期 Google 未支持 Flutter Module 时,进行混合开发也只存在这一种模式。
后来 Google 对混合开发有了更好的支持,除了 Flutter Application,还支持 Flutter Module。所谓 Flutter Module,恰如其名,就是支持以模块化的方式将 Flutter 引入原生工程中,**它的产物就是 iOS 下的 Framework 或 Pods、Android 下的 AAR,原生工程就像引入其余第三方 SDK 那样,使用 Maven 和 Cocoapods 引入 Flutter Module 便可。**从而实现真正意义上的三端分离的开发模式。
为了问题的简洁性,咱们这里暂时不考虑生命周期的统一性和通讯层的实现,而除此以外,混合导航栈主要须要解决如下四种场景下的问题:
Native -> Flutter,这种状况比较简单,Flutter Engine 已经为咱们提供了现成的 Plugin,即 iOS 下的 FlutterViewController 与 Android 下的 FlutterView(自行包装一下能够实现 FlutterActivity),因此这种场景咱们直接使用启动了的 Flutter Engine 来初始化 Flutter 容器,为其设置初始路由页面以后,就能够以原生的方式跳转至 Flutter 页面了。
// Existing code omitted.
// 省略已经存在的代码
- (void)showFlutter {
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
复制代码
Flutter -> Flutter,业内存在两种方案,后续咱们会详细介绍到,分别是:
Flutter -> Native,须要注意的时,这里的跳转实际上是包含了两种状况:
如上图,这种状况相对复杂,咱们须要使用 MethodChannel 让 Dart 与 Platform 侧进行通讯,Dart 发出 open 或 close 的指令后由原生侧执行相应的逻辑。
Native -> Native,这种状况没有什么好说的,直接使用原生的导航栈便可。
为了解决混合栈问题,以及弥补 Flutter 自身对混合开发支持的不足,业内提出了一些混合栈框架,总得来讲,离不开这四种混合模式:
下面,一一来谈谈它们的原理与优缺点。
Flutter Boost 是闲鱼团队开源的 Flutter 混合框架,成熟稳定,业内影响力高,在导航栈的处理思路上没有绕开咱们在 3.2 节中谈及的混合栈原理,但须要注意的是,当 Flutter 跳转 Flutter 时,它采用的是 new 一个新的 FlutterViewController 后使用原生导航栈跳转的方式,以下图所示:
这么作的好处是使用者(业务开发者)操做 Flutter 容器就如同操做 WebView 同样,而 Flutter 页面就如同 Web 页面,逻辑上简单清晰,将全部的导航路由逻辑收归到原生侧处理。以下图,是调用 open 方法时 Flutter Boost 的时序图(关键函数路径),这里能够看到两点信息:
可是它也有缺点,就是每次打开 Flutter 页面都须要 new 一个 ViewController,在连续的 Flutter 跳转 Flutter 的场景下有额外的内存开销。针对这个问题,又有团队开发了 Flutter Thrio。
上面咱们说到,Flutter 跳转 Flutter 这种场景 Flutter Boost 存在额外的内存开销,故哈啰出行团队今年4月开源了 Flutter Thrio 混合框架,其针对 Flutter Boost 作出的最重要的改变在于:Flutter 跳转 Flutter 这种场景下,Thrio 使用了 Flutter Navigator 导航栈。以下图所示:
在连续的 Flutter 页面跳转场景下,内存测试图表以下:
从这张图表中咱们能够获得如下几点信息:
可见,在这种场景下,Thrio 仍是作出了必定的优化的。但与之带来的,就是实现的复杂性。咱们谈到 Flutter Boost 的优势是简单,路由所有收归原生导航栈。而 Flutter Thrio 混用了原生导航栈和 Flutter Navigator,所以实现会相对更复杂一下。这里我梳理了一下 Flutter Thrio open 时关键函数路径,能够看到,Thrio 的导航管理确实是复杂了一些。
以上咱们谈及的两种混合框架都是单引擎的,对应的,也存在多引擎的框架。在谈多引擎以前,仍是须要先介绍一下关于 Engine、Dart VM、isolate 几个前置知识点。
在第一篇章中咱们没有涉及到 Engine 层的源码分析,而着重篇幅去讲解 Framework 层的原理,一是为了第一章的连贯性,二是此处也会单独说到 Engine,仍是最好放在此时讲解会更便于记忆与理解。
(a)Dart 虚拟机建立完成以后,须要建立 Engine 对象,而后会调用 DartIsolate::CreateRootIsolate()
来建立 isolate。 (b)每个 Engine 实例都为 UI、GPU、IO、Platform Runner 建立各自新的 Thread。 (c)isolate,顾名思义,内存在逻辑上是隔离的。 (d)isolate 中的 code 是按顺序执行的,任何 Dart 程序的并发都是运行多个 isolate 的结果。固然咱们能够开启多个 isolate 来处理 CPU 密集型任务。
根据(a)咱们能够推出:(1) 每一个 Engine 对应一个 isolate 对象,即 Root Isolate。 根据(b)咱们能够推出:(2) Engine 是一个比较重的对象(前文也有所说起)。 根据(c)和 (1) 咱们能够推出:(3) Engine 与 Engine 之间相互隔离。 根据(d)和 (3) 咱们能够推出:(4) Engine 没有共享内存的并发,没有竞争的可能性,不须要锁,也就不存在死锁问题。
好啦,记住这四个结论,咱们再来看看 window。
window 是绘图的窗口,也是链接 Flutter Framework(Dart)与 Flutter Engine(C++)的窗口 (5)。
从类的定义上来看,window 是链接 Framework 与 Engine 的窗口。在 Framework 层,window 指的是 ui.window
单例对象,源码文件是 window.dart。而在 Engine 层,源码文件是 window.cc,二者交互的 API 不多,可是一一对应:
/// window.dart
class Window {
String/*!*/ _defaultRouteName() native 'Window_defaultRouteName';
void scheduleFrame() native 'Window_scheduleFrame';
String _sendPlatformMessage(String/*!*/ name,
PlatformMessageResponseCallback/*?*/ callback,
ByteData/*?*/ data) native 'Window_sendPlatformMessage';
ByteData/*?*/ getPersistentIsolateData() native 'Window_getPersistentIsolateData';
/// ...
}
复制代码
// window.cc
void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
natives->Register({
{"Window_defaultRouteName", DefaultRouteName, 1, true},
{"Window_scheduleFrame", ScheduleFrame, 1, true},
{"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
{"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
{"Window_render", Render, 2, true},
{"Window_updateSemantics", UpdateSemantics, 2, true},
{"Window_setIsolateDebugName", SetIsolateDebugName, 2, true},
{"Window_reportUnhandledException", ReportUnhandledException, 2, true},
{"Window_setNeedsReportTimings", SetNeedsReportTimings, 2, true},
{"Window_getPersistentIsolateData", GetPersistentIsolateData, 1, true},
});
}
复制代码
能够发现,这些主要是 Framework 层调用 Engine 层中 Skia 库封装后的相关 API。那就不得不说说它的第二层含义——做为绘图的窗口。
从功能上来看,在界面绘制交互意义上,window 也是绘图的窗口。在 Engine 中,绘图操做输出到了一个 PictureRecorder
的对象上;在此对象上调用 endRecording()
获得一个 Picture
对象,而后须要在合适的时候把 Picture
对象添加(add)到 SceneBuilder
对象上;调用 SceneBuilder
对象的 build()
方法得到一个 Scene
对象;最后,在合适的时机把 Scene
对象传递给 window.render()
方法,最终把场景渲染出来。
实例代码以下:
import 'dart:ui';
void main(){
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
Paint p = Paint();
p.strokeWidth = 30.0;
p.color = Color(0xFFFF00FF);
canvas.drawLine(Offset(300, 300), Offset(800, 800), p);
Picture picture = recorder.endRecording();
SceneBuilder sceneBuilder = SceneBuilder();
sceneBuilder.pushOffset(0, 0);
sceneBuilder.addPicture(new Offset(0, 0), picture);
sceneBuilder.pop();
Scene scene = sceneBuilder.build();
window.onDrawFrame = (){
window.render(scene);
};
window.scheduleFrame();
}
复制代码
综上,根据(1)(3)(5)咱们能够得出下图的多引擎模式:
它有如下几个特征:
根据这三个特征,咱们能够设想一下其通讯层的实现,假设存在两个引擎,每一个引擎内又存在两个 FlutterVC,每一个 FlutterVC 内又存在两个 Flutter 页面,那这种场景下的跳转就会变得很是复杂(下图出自 Thrio 开源仓库中的README):
因此显而易见的,咱们不能否认 Engine 之间的逻辑隔离带来了模块间自然的隔离性,可是问题也有许多:
首先如上图所示,通讯层设计会异常复杂,并且通讯层的核心逻辑依然是须要放在原生侧来实现,如此便必定程度上失去了跨端开发的优点。
其次,咱们反复提到 Engine 是一个比较重的对象,启动多个 Flutter Engine 会致使资源消耗过多。
最后,因为 Engine 之间没有共享内存,这种自然的隔离性其实弊大于利,在混合开发的视角下,一个 App 须要维护两套缓存池——原生缓存池与 DartVM 所持有的缓存池,可是随着开启多 Engine 的介入,后者缓存池的资源又互不相通,致使资源开销变得更加巨大。
为了解决传统的多 Engine 模式所带来的这些问题,又有团队提出了基于 View 级别的混合模式。
基于 View 级别的混合模式,核心是为每一个 window 加入 windowId 的概念,以便它们去共享同一份 Root Isolate。咱们刚才说到,一个 isolate 具备一个 ui.window
单例对象,那么只须要作一点修改,把 Flutter Engine 加入 ID 的概念传给 Dart 层,让 Dart 层存在多个 window,就能够实现多个 Flutter Engine 共享一个 isolate 了。
以下图所示:
这样就能够真正实现 View 级别的混合开发,能够同时持有多份 FlutterViewController,且这些 FlutterVC 能够内存共享。
那缺点也比较明显,咱们须要对 Engine 代码作出修改,维护成本会很高。其次,多 Engine 的资源消耗问题在这种模式下也是须要经过对 Engine 不断裁剪来解决的。
Dart 自然支持两种编译模式,JIT 与 AOT。
所谓 JIT,Just In Time,即时编译/运行时编译,在 Debug 模式中使用,能够动态下发和执行代码,可是执行性能受运行时编译影响。
所谓 AOT,Ahead Of Time,提早编译/运行前编译,在 Release 模式中使用,能够为特定平台生成二进制代码,执行性能好、运行速度快,但每次执行都须要提早编译,开发调试效率低。
对应的 Flutter App 存在三种运行模式:
所以,咱们能够看出,在开发调试过程当中,咱们须要使用支持 JIT 的 Debug 模式,而在生产环境中,咱们须要构建包为支持 AOT 的 Release 模式以保证性能。
那么,这对咱们的集成与构建也提出了必定的要求。
所谓集成,指的是混合项目中,将 Flutter Module 的产物集成到原生项目中去,存在两种集成方式,区别以下:
能够发现源码集成是 Flutter dev 分支须要的,可是产物集成是 Flutter dev 之外的分支须要的。在这里,咱们的混合项目须要同时支持两种不一样的集成工程,在 Flutter dev 分支上进行源码集成开发,而后依赖抽取构建产物发布到远程,如 iOS 构建成 pods 发布到 Cocoapods 对应的仓库,而 Android 构建成 AAR 发布到 Maven 对应的云端。因而,其余分支的工程直接 gradle 或者 pod install 就能够更新 Flutter 依赖模块了。
固然,咱们说到运行模式存在 Debug、Release、Profile 三种,其对应的集成产物也会区分这三种版本,但因为产物集成没法调试,集成 Debug 版本和 Profile 版本没有意义,所以依赖抽取发布时只须要发布 Release 版本的产物就好。
在整套「Fan 直播」Flutter 混合项目搭建以后,咱们造成了一套初具雏形的 Flutter 工做流。在将来,咱们也会不断完善 Flutter 混合开发模式,积极参与到 Flutter 的生态建设中去。