1.5 使用混合栈框架开发
1.5.1 为什么需要混合方案
具有一定规模的App 通常有一套成熟通用的基础库,尤其是阿里巴巴App,一般需要依赖很多体系内的基础库。使用Flutter 重新开发App 的成本和风险都较高。所以,在Native App 进行渐进式迁移是稳健型方式。闲鱼在实践中沉淀出一套自己的混合技术方案。在此过程中,闲鱼跟GoogleFlutter 团队进行密切的沟通,听取了他们的一些建议,同时也针对自身业务情况进行方案的选型以及具体的实现方法。
1.5.2 Google 官方提出的混合方案
1.基本原理
Flutter 技术链主要由C++实现的Flutter Engine 和Dart 实现的Framework 组成。Flutter Engine 负责线程管理、Dart VM 状态管理和Dart代码加载等工作。而Dart 代码所实现的Framework 则是业务接触到的主要API,如Widget 等概念就是在Dart 层面的Framework 内容。
一个进程里面最多只会初始化一个Dart VM。然而,一个进程可以有多个Flutter Engine,多个Engine 实例共享同一个Dart VM。
我们来看具体实现方法,在iOS 中,每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程(理论上线程可以复用)去运行Dart 代码。Android 中的Activity 也有类似的效果。如果启动多个引擎实例,此时Dart VM 依然是共享的,只是不同Engine 实例加载的代码运行在各自独立的Isolate 中。
2.Google 官方给出的建议
(1)引擎深度共享
在混合方案方面,Flutter 官方给出的建议是从长期来看,应该支持在同一个引擎支持多窗口绘制的能力, 至少在逻辑上做到FlutterViewController 共享同一个引擎的资源。换句话说,希望所有的绘制窗口共享同一个主Isolate。
但Google 官方给出的长期建议目前来说没有很好的支持。
(2)多引擎模式
在混合方案中,我们解决的主要问题是如何处理交替出现的Flutter 和Native 页面。Google 工程师给出了一个Keep It Simple 的方案:对于连续的Flutter 页面(Widget),只需要在当前FlutterViewController 中打开即可,对于间隔的Flutter 页面,选择初始化新的引擎。
例如,进行下面一组导航操作:
Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3
只需在Flutter Page1 和Flutter Page3 中创建不同的Flutter 实例即可。
这个方案的好处是简单易懂,逻辑清晰;但也有潜在的问题,如果一个Native 页面和一个Flutter 页面一直交替进行,那么Flutter Engine 的数量会呈线性增加,而Flutter Engine 本身是一个比较重的对象。
(3)多引擎模式的问题
- 冗余的资源问题。多引擎模式下,每个引擎之间的Isolate 是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,若每个引擎都维护自己的一份图片缓存,则内存压力将非常大。
- 插件注册的问题。插件依赖Messenger 传递消息,而Messenger 是由FlutterViewController ( Activity ) 实现的。如果有多个FlutterViewController,插件的注册和通信将会变得混乱、难以维护,消息传递的源头和目标也会变得不可控。
- Flutter Widget 和Native 的页面差异化问题。Flutter 的页面是Widget,Native 的页面是VC。从逻辑上来说,我们希望消除Flutter 页面与Naitve页面的差异,否则在进行页面埋点和其他一些统一操作的时候,都会遇到额外的复杂度。
- 增加页面之间通信的复杂度。如果所有Dart 代码都运行在同一个引擎实例中,它们共享一个Isolate,则可以用统一的编程框架进行Widget之间的通信,多引擎实例也增加复杂度。
因此,综合多方面考虑,闲鱼并没有采用多引擎混合方案。
3.现状与思考
考虑到多引擎存在的一些实际问题,所以闲鱼目前采用的混合方案是共享同一个引擎。这个方案基于这样一个事实:在任何时候最多只能看到一个页面,当然对于些特定的场景,可以看到多个ViewController,但是这些特殊场景我们这里不讨论。
可以这样简单地理解这个方案:把共享的Flutter View 当成一个画布,然后用一个Native 的容器作为逻辑的页面。每次在打开一个容器的时候,通过通信机制通知Flutter View 绘制成当前的逻辑页面,然后将FlutterView 放到当前容器里面。
老方案在Dart 侧维护了一个Navigator 栈的结构,如图1-20 所示。栈数据结构的特点是每次只能从栈顶操作页面,每一次在查找逻辑页面的时候,如果页面不在栈顶,则需要往回退栈。如此会导致中途被退栈的页面状态丢失,这个方案无法支持同时存在多个平级逻辑页面的情况,因为在切换页面的时候必须从栈顶操作,无法在保持状态的同时进行平级切换。
举个例子:有两个页面A 和B,当前页面B 在栈顶。切换到页面A需要把页面B 从栈顶Pop 出去,此时页面B 的状态丢失,如果想切回页面B,只能重新打开,页面B 之前页面的状态无法维持住。这也是老方案最大的一个局限。
如在Pop 的过程中,可能会把Flutter 官方的Dialog 误杀,这也是一个问题。
而且基于栈的操作,我们依赖对Flutter 框架的一个属性修改,让这个方案具有了侵入性的特点,这是另一个问题。
图1-20