应用开发的混合技术方案:FlutterBoost
为什么采用混合方案
一定规模的应用通常都会有一套成熟且通用的核心库,尤其是阿里巴巴的应用,一般需要依赖很多核心库系统。那么使用Flutter从头开发应用程序的成本和风险就更高。因此,Native App 中的渐进式迁移是在现有本机应用程序中应用 Flutter 技术的一种可靠方法。
闲鱼在实践中形成了自己的混合技术解决方案。在这个过程中,我们与Google Flutter团队进行了密切的沟通,听取了一些官方的建议,同时也根据我们业务的具体情况选择了解决方案并实施。
官方提出混合方案
1 基本原理
Flutter技术链主要由C++实现的Flutter Engine和Dart实现的Framework组成(我们不参与其配套编译构建工具的讨论这里)。 Flutter Engine 负责管理线程、管理 Dart VM 状态以及加载 Dart 代码。 Dart代码实现的框架是公司接触到的主要API。像 Widget 这样的概念是 Dart 级别框架的内容。
每个进程只能初始化一个 Dart VM。但是,一个进程可以有多个 Flutter Engine,并且多个 Engine 实例共享同一个 Dart VM。
我们看一下具体的实现。每次在 iOS 上初始化 FlutterViewController 时,都会相应地初始化引擎,这意味着将会有新的线程(理论上线程可以重用)来运行 Dart 代码。 Android 上的类似活动也会产生类似的效果。如果您运行多个引擎实例,请注意,此时 Dart VM 仍然是共享的,但不同引擎实例加载的代码在单独的隔离区中运行。
2官方提案
深度分享
对于混合解决方案,我们与Google讨论了一些可能的解决方案。 Flutter官方的建议是,从长远来看我们应该支持在同一个引擎中支持在多个窗口中绘图的能力,至少在逻辑上让FlutterViewController共享同一个引擎的资源。换句话说,我们希望所有绘图窗口共享相同的主隔离。
但官方给出的长期建议目前并没有很好的支持。
多引擎模式
我们在混合方案中解决的主要问题是如何处理交替的Flutter和Native页面。 Google 工程师提供了一个 Keep It Simple 解决方案:对于连续的 Flutter 页面(Widget),我们只需在当前 FlutterViewController 中打开它们,对于间隔的 Flutter 页面,我们初始化一个新引擎。
例如,我们执行以下一组导航操作:
我们只需要在 Flutter Page1 和 Flutter Page3 上创建不同的 Flutter 实例即可。
该方案的优点是易于理解,逻辑清晰,但也存在潜在的问题。如果原生页面和 Flutter 页面
不断交替,Flutter Engine 的数量就会线性增加,Flutter Engine 本身就是一个相当重的物体。
多引擎模式下的问题
- 冗余资源的问题。多电机模式下各电机之间的隔离是相互独立的。从逻辑上来说,这并没有什么坏处,但引擎底层实际上维护着图像缓存等内存密集型对象。想象一下,每个引擎都维护自己的图像缓存,内存压力会非常大。
- 插件注册问题。该插件依赖Messenger来传递消息,Messenger目前是使用FlutterViewController(Activity)实现的。如果你有多个FlutterViewController,插件注册和通信会变得混乱且难以维护,消息传递的来源和目的地也变得不可控。
- Flutter Widget 和 Native 之间的页面区分问题。 Flutter的页面是Widget,Native的页面是VC。从逻辑上讲,我们希望消除 Flutter 和 Naitve 页面之间的差异,否则在执行页面嵌入等统一操作时会遇到额外的复杂性。
- 增加了页面之间通信的复杂性。如果所有 Dart 代码都在引擎的同一实例上运行,则它们共享一个 Isolate,并且可以使用单个编程框架在小部件之间进行通信。多个引擎实例也使问题变得复杂。
经过多方面的考虑,我们没有采用多引擎混合动力的方案。
现状与思考
我们已经提到了多引擎存在一些实际问题,所以闲鱼目前采用的混合解决方案是共享同一台引擎。该解决方案基于这样一个事实:我们在任何给定时间最多只能看到一页。当然,在某些特定场景下你可能会看到多个ViewController,但我们在这里不讨论这些特殊场景。
我们对这个方案的理解很简单:我们将共享的 Flutter View 视为画布,然后使用原生容器作为逻辑页面。每次我们打开一个容器时,我们都会通过通信机制通知Flutter View绘制当前逻辑页面,然后将Flutter View插入到当前容器中。
该方案无法支持多个水平逻辑页面同时存在,因为切换页面时必须从栈顶开始工作,无法进行有状态的水平切换。例如:有两个页面A和B,B当前位于堆栈的顶部。切换到A需要从栈顶弹出B。此时,状态B丢失。如果我们想切换回B,只能再次打开B,而无法保持上一页的状态。
如果Flutter官方的dialog在pop过程中会被意外杀死。此外,对于基于堆栈的操作,我们依赖于修改Flutter框架的属性,这使得该解决方案具有侵入性。
具体细节可以参考旧方案开源项目地址:
新一代混合技术解决方案FlutterBoost
1重构方案
随着闲鱼推广颤动化,站点场景更加复杂旧方案的局限性和一些问题逐渐显露出来。因此我们推出了一种新的混合技术解决方案,代号为 FlutterBoost(向 C++ Boost 库致敬)。我们对于这个新的混合解决方案的主要目标是:
- 可复用且多功能的混合解决方案
- 支持更复杂的混合模式,例如Tab主页支持
- 非侵入性解决方案:无依赖修改Flutter解决方案♿❀ⓓ通用生命页面周期
- 统一和明确设计理念
与旧方案类似,新方案依然采用共享引擎模型实现。主要思想是原生容器的容器通过消息控制Flutter页面容器的容器,达到原生容器和Flutter容器同步的目的。我们希望Flutter渲染的内容由Naitve容器来管理。
为了简单起见,我们希望 Flutter 容器感觉像一个浏览器。填写页面地址,容器即可管理页面绘制。在Native端,我们只需要关心初始化容器,然后设置容器对应的页面的flag即可。? :基于通道的消息通信
4Dart 层概念
- 容器:Flutter 用于容纳 widget 的容器。具体实现是Navigator派生类——
- Container Manager:Flutter容器管理,提供show、remove等API。
- Coordinator:协调器,接收message消息,负责容器管理器状态管理调用。
- 消息传递:基于通道的消息通信
5 理解页面
Native 和 Flutter 中表示页面的对象和概念不一致。在Native中,我们的页面概念一般就是ViewController和Activity。对于 Flutter 来说,我们的概念页面是一个 Widget。我们希望统一页面的概念或者弱化、抽象Flutter自带的widget对应的页面的概念。也就是说,当存在原生页面容器时,FletteBoost 保证会有一个 widget 作为容器的内容。因此,我们在理解和进行路由操作时,应该参考Native容器。 Flutter Widget 取决于本机页面容器的状态。
所以当我们谈论 FlutterBoost 概念中的页面时,我们指的是 Native 容器及其附加的 Widget。所有页面路由操作,打开或关闭页面,实际上都是对原生页面容器的直接操作。无论路由请求来自哪里,最终都会传递给Native来实现路由操作。这也是为什么连接FlutterBoost时需要实现Platform协议的原因。
另一方面,我们无法控制业务代码通过Flutter自带的Navigator推送新的widget。对于业务直接使用Navigator控制Widget而不使用FlutterBoost的情况,包括Dialog等非全屏的Widget,我们建议业务自己负责管理其状态。这种类型的 widget 不属于 FlutterBoost 定义的页面概念。
了解和使用 FlutterBoost 来理解这里的站点概念是至关重要的。
6 与旧解决方案的主要区别
我们已经提到,旧解决方案在 Dart 层中维护单个 Navigator 堆栈结构,用于切换 widget。新的解决方案在 Dart 端引入了容器的概念。它不再使用堆栈结构来维护现有页面,而是以平面键值映射的形式维护所有当前页面。每个页面都有一个唯一的 ID。这种结构天然支持分页和交换,不再受到栈顶操作的问题。之前pop引起的一些问题很容易解决。无需依赖编辑Flutter源码来进行页面堆栈操作,从而消除了侵入式实现。
其实我们引入的容器是一个Navigator,也就是说原生容器对应的是Navigator。那么它是如何做到的呢?
7 实现多个导航器
Flutter 提供了一个用于自定义较低级别导航器的接口。我们实现了一个用于管理多个导航器的对象。当前最多可见一个 Flutter Navigator,该 Navigator 所包含的页面就是我们当前可见容器对应的页面。
原生容器和Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当创建原生容器时,也会创建一个 Flutter 容器,并且它们通过相同的 id 链接。当原来的容器被销毁时,Flutter容器也会被销毁。 Flutter容器的状态遵循原生容器,我们称之为原生控制器。管理器管理和切换当前显示在屏幕上的容器。
为了描述创建新页面的过程,我们使用一个简单的示例:
- 创建一个原生容器(iOS ViewController、Android Activity 或 Fragment)。
- 原生容器通过消息机制通知 Flutter 协调器有新容器创建。然后,
- Flutter 容器管理器会收到通知,并负责创建适当的 Flutter 容器并将适当的小部件页面加载到其中。
- 当Native容器显示在屏幕上时,容器会向Flutter协调器发送消息,宣布要显示的页面的id。
- Flutter Container Manager 会找到 ID 匹配的 Flutter Container,并将其设置为可见的前台容器。
这是创建新页面的主要逻辑。销毁和进入后台等操作也由本机容器事件控制。
总结
目前,FlutterBoost在生产环境中支持闲鱼客户端中所有基于Flutter的开发服务,为更复杂的混合场景提供支持,为亿万用户提供稳定的服务。
项目之初,我们希望FlutterBoost能够解决混合Native App模式下访问Flutter的常见问题。所以我们把它做成了一个可复用的Flutter插件,希望能够吸引更多感兴趣的朋友参与Flutter社区的建设。在有限的篇幅内,我们分享闲鱼在Flutter混合技术解决方案中积累的经验和代码。欢迎有兴趣学习的同学积极与我们交流学习。
与性能相关的高级补充
1
在两个Flutter页面之间切换时,由于我们只有一个Flutter View,所以需要对上一页进行快照并保存。如果一个Flutter页面进行多次截图,会占用大量内存。这里我们使用二级文件内存缓存策略,我们只在内存中存储2-3张截图,其余写入的文件按需加载。这样我们就可以在保证用户体验的同时,保持相对稳定的内存水平。
说到页面渲染性能,Flutter的AOT的优势就展现得淋漓尽致。在快速切换页面时,Flutter 可以非常灵敏地响应页面切换,顺理成章地营造出多个 Flutter 页面的感觉。对
项目之初,我们是基于闲鱼目前使用的Flutter版本进行开发的,然后我们做了升级测试到1.0版本,目前为止还没有发现任何问题。
3 做法
只要项目集成了Flutter,使用官方的依赖方式以插件的形式实现FlutterBoost是非常方便的。该方法只需要在项目中放入少量代码即可完成。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。