适用于多场景的Flutter复用engine方案【上篇】

4,263 阅读6分钟

本文所有的说明是基于iOS开发角度,原创文章,转载请注明来源

一、背景

一个大前提,此方案是解决原生和flutter混合栈开发项目,多engine内存占用问题而设计的。所以适用于flutter混合项目,纯flutter项目无需使用。

首先来简单解答一下,为啥混合栈会存在内存问题呢?

在纯的flutter工程中,只有一个flutterViewController,所以engine也对应一个,结构如下

在混合项目中,会有多个flutterViewController,每个flutter vc对应一个engine,所以是一个多engine的结构

在iOS端没创建一个flutterViewController和engine Release环境下会增加20M左右,所以如果一直push新的flutter vc会造成内存快速暴涨。特别在iOS上如果flutter vc移除的时候没有很好清理engine占用的内存,会存在内存泄漏。

彻底销毁engine的方式如下

冗余的资源问题,多引擎模式下每个引擎之间的Isolate是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,每个引擎都维护自己一份图片缓存,内存压力将会非常大。

页面之间通信的复杂度。如果所有Dart代码都运行在同一个引擎实例,变量可以进行共享,通信会变得很容易,多engine给flutter页面之间进行通信带来更大的成本。

所以在混合栈项目中,研究落地复用engine方案成为各大团队不可避免的技术难关。

那复用engine需要面对的难点有哪些呢?

1、首先复用engine,就是只实例化一个flutter engine对象,每个flutter vc挂载一个engine,那flutter vc实例是否需要复用

2、如何管理路由栈,支持打开任意原生和flutter页面,处理原生打开flutter页面,flutter页面打开flutter页面,flutter页面打开原生页面;页面回退如何处理;跳转动画实现

3、如何处理多个flutter页面平级存在场景,比如一级tab上存在2个以上flutter页面,又或者scroll page里面存在原生和flutter页面,页面之间切换动画实现

4、如何设计,对业务侵入小,可以快速降级到多engine

业内开源的方案如何设计?

闲鱼团队flutter boost方案是目前国内影响力最大的方案

flutterViewController不复用,通过移动同一个engine实例来实现,使用原生端进行主要的路由管理和同步。

反馈缺陷:闪烁问题;flutter vc销毁存在内存泄漏;

                                                                来源网络

哈罗团队Thrio方案

支持了多engine和单engine两种模式,实现方案比boost更复杂,共同点都是业务端通知到原生端进行统一的路由跳转消息分发,最大区别是Thrio在flutter页面打开flutter页面时候,没有跟boost那样创建新的flutterViewController实例,而是通知dart端进行跳转,所以连续打开flutter页面,内存表现Thrio是比boost优秀的。

但是不管是Thrio还是boost,对于平级的多flutter页面没有进行支持,更适用于层级页面跳转。

二、powerful engine方案

1、为了能解决各种复杂的场景:

  • 支持引导页使用flutter开发
  • 支持app tab上存在多个平级flutter页面和原生页面混合
  • push或者present任意原生或者flutter页面
  • 支持自己的路由协议和插件协议
  • 支持局部flutter页面engine复用,支持dart端同一套逻辑支持iOS单engine,安卓多engine。
  • 可以随时单engine和多engine切换

2、页面布局结构

原生端页面结构

dart端页面结构

说明:

  • 不带序号vc代表会创建不同的实例对象,不共用。带相同序号vc代表共用实例对象,在不同页面之中移动;带不同序号代表不共用实例对象。engine是共用一个实例对象。也就是说在一级tab页面的flutter vc是独立的实例对象,engine进行移动。但是在二级页面跳转,是共用同一个flutter vc实例对象,对flutter vc进行移动。
  • 在原生端进行路由栈操作的flutter页面载体都是一个容器viewController,并不是直接使用flutterViewController,这样的设计决定了所有页面跳转动画效果和侧滑返回都是由container vc来实现的,dart端抛弃所有push动画效果,采用无动画页面切换。
  • 跟boost一样,dart端所有关于的flutter页面操作都要传给原生端进行统一管理
  • dart端针对平级flutter页面采用indexedStack组件进行页面切换,原生页面采用空白widget占位设计。原生端二级页面虽然是多导航栈结构,但是所有flutter页面在dart端表现都是在一个Navigator栈进行管理,也就是说原生端实现了engine跨栈移动策略。

3、路由通信

通信管理时序图
说明:

  • 所有路由操作都汇总到原生端进行管理
  • 每个页面都有一个pageId作为唯一标识,iOS使用的是container vc实例对象的hash值
  • 页面生命周期使用container vc的生命周期为准,并同步到dart端

4、单engine和多engine快速切换如何实现

前面说明了如何进行平级页面和多级页面路由栈管理和通信策略,那剩下需要解答如何使用同一套dart支持单engine和多engine,甚至局部flutter复用engine。从上面可知dart端rooter widget是indexedStack组件,这是整个flutter栈的根节点,那只需要把根节点替换成各自flutter页面的widget,就能轻易切换多engine模式。

flutter启动main.dart可以设置初始路由,用来加载不同widget,所以可以通过设置初始路由来标识engine模式。dart端是使用ui.window.defaultRouteName,iOS端是flutter vc调用setInitialRoute方法设置。

这里flutter iOS端有一个坑,如果预加载engine是无法自己设置初始路由的,虽然官方提供了方式,但是在iOS上都验证无效。所以必须通过初始化flutter vc来设置初始路由,所有iOS这边预加载engine采用了一个取巧方案,通过初始化一个临时flutter vc来预加载一个engine,并设置初始路由。获取到engine之后就释放临时flutter vc实例。

经过设置初始路由,dart端就可以根据路由来加载对应的widget,自然也可以加载indexedStack,达到由原生自由控制单engine和多engine模式切换。

5、对比boost和Thrio

  • 由于打开页面都是共用同一个flutter vc的实例,完美解决了内存泄漏和暴涨问题
  • dart端使用indexedStack组件统一管理widget,极大简化了flutter接入成本
  • 原生端路由栈不直接操作flutter vc,对engine和flutter vc移动做到无感知
  • 单engine和多engine模式切换简单而且可以很好与项目其它独立flutter模块结合,不相互耦合影响