得益于 Flutter 优秀的跨渠道表现,混合开发在如今的 App 中随处可见,如最近微信发布的小程序新烘托引擎 Skyline 发布正式版也在底层烘托上运用了 Flutter,声称烘托速度提升50%。

在现有的原生 App 中引入 Flutter 来开发不是一件简单的事,需求处理混合形式下带来的种种问题,如路由栈办理、包体积和内存突增等;别的还有一种特别的状况,一个开始就由 Flutter 来开发的 App 也有可能在后期混入原生 View 去开发。

我地点的团队现在便是处于这种状况,Flutter 现在在功能表现上面还不行完美,整体页面还不行流通,并且在一些复杂的页面场景下会呈现比较严重的发热行为,尽管现在 Flutter 团队发布了新的烘托引擎 impeller,它在 iOS 上表现优异,流通度有了质的提升,但还是无法完全处理一些功能问题且 Android 下 impeller 也还没开发完成。

为了应对当下呈现的困局和以后可能呈现的未知问题,咱们希望经过混合形式来扩宽更多的可能性。

路由办理

混合开发下最难处理的便是路由问题了,咱们知道原生和 Flutter 都有各自的路由办理系统,在原生页面和 Flutter 页面穿插的状况下怎么统一办理和相互交互是一大难点。现在比较盛行的单引擎计划,代表结构是闲鱼团队出品flutter_boost;flutter 官方代表的多引擎处理计划 FlutterEngineGroup。

单引擎计划 flutter_boost

flutter_boost 经过复用 Engine 到达最小内存的意图,下面这张图是它的规划架构图(图片来自官方)

Flutter 混合架构方案探索

在引擎处理上,flutter_boost 界说了一个通用的 CacheId:”flutter_boost_default_engine”,当原生需求跳转到 Flutter 页面时,经过FlutterEngineCache.getInstance().get(ENGINE_ID); 获取同一个 Engine,这样不管打开了多少如图中的 A、B、C 的 Flutter 页面时,都不会发生额定的Engine内存损耗。

public class FlutterBoost {
    public static final String ENGINE_ID = "flutter_boost_default_engine";
    ...
}

别的,双端都注册了导航的接口,经过Channel来告诉,用于恳求路由改变、页面回来以及页面的生命周期处理等。在这种形式下,这一层Channel的接口处理是重点。

多引擎计划 FlutterEngineGroup

为了应对内存爆破问题,官方对多引擎场景做了优化,FlutterEngineGroup应运而生,FlutterEngineGroup下的 Engine 共用一些通用的资源,例如GPU 上下文、线程快照等,生成额定的 Engine 时,声称内存占用缩小到 180k。这个程度,基本能够视为正常的损耗了。

以上图中的 B、C 页面为例,两者都是 Flutter 页面,在 FlutterEngineGroup 这种处理下,由于它们地点的 Engine 不是同一个,这会发生完全的隔离行为,也便是 B、C 页面运用不同的仓库,处在不同的 Isolate 中,两者是无法直接进行交互的。

多引擎的优点是:它能够抹掉上图所示的 F、E、C 和 D、A 等内部路由,每次新增 Flutter 页面时,悉数回调到原生,让原生生成新的 Engine 去承载页面,这样路由的办理悉数由原生去处理,一个 Engine 只对应一个 Flutter 页面。

但它也会带来一些额定的处理,像上面提到的,处在不同 Engine 下的Flutter 页面之间是无法直接交互的,假如涉及到需求告诉和交互的场景,还得经过原生去转发。

关于FlutterEngineGroup的更多信息,能够参阅官方说明。

功能对比

官方声称 FlutterEngineGroup 创建新的 Engine 只会占用 180k 的内存,那么是不是真就如它所说呢?下面咱们来针对上面这两种计划做一个内存占用测验

flutter_boost

测验机型:OPPO CPH2269

测验代码:github.com/alibaba/flu…

内存 dump 指令: adb shell dumpsys meminfo com.idlefish.flutterboost.example

条件 PSS RSS 最大改变
1 Native 88667 165971
+26105 +28313 +27M
1 Native + 1 Flutter 114772 194284
-282 +1721 +1M
2 Native + 2 Flutter 114490 196005
+5774 +5992 +6M
5 Native + 5 Flutter 120264 201997
+13414 +14119 +13M
10 Native + 10 Flutter 133678 216116

第一次加载 Flutter 页面时,添加 27M 左右内存,尔后多开一个页面内存添加呈现从 1M -> 2M -> 2.6 M 这种越来越陡的趋势(数值仅仅参阅,由于其中有 Native 页面,只看趋势改变上看)

FlutterEngineGroup

测验机型:OPPO CPH2269

测验代码:github.com/flutter/sam…

内存 dump 指令: adb shell dumpsys meminfo dev.flutter.multipleflutters

条件 PSS RSS 最大改变
1 Native 45962 140817
+29822 +31675 +31M
1 Native + 1 Flutter 75784 172492
-610 +2063 +2M
2 Native + 2 Flutter 75174 174555
+7451 +7027 +3.7M
5 Native + 5 Flutter 82625 181582
+8558 +7442 +8M
10 Native + 10 Flutter 91183 189024

第一次加载 Flutter 页面时,添加 31M 左右内存,尔后多开一个页面内存添加呈现从 1M -> 1.2M -> 1.6 M 这种越来越陡的趋势(数值仅仅参阅,由于其中有 Native 页面,只看趋势改变上看)

定论

两个测验运用的是不同的 demo 代码,不能经过数值去得出孰优孰劣。但经过数值的表现,咱们基本能够确认,两个计划都不会带来异常的内存暴涨,完全在能够承受的规模。

PlatformView

PlatformView 也可完成混合 UI,Flutter 中的 WebView 便是经过 PlatformView 这种办法引入的。

PlatformView 答应咱们向 Flutter 界面中刺进原生 View,在一个页面的最外层包裹一层 PlatformView,路由的办理都由 Flutter 来处理。这种办法下没有额定的 Engine 发生,是最简单的混合办法。

但它也有缺陷,不适合主 Native 混 Flutter 的场景,而现在大多都是以主 Native 混 Flutter的场景为主。别的,PlatformView 因其底层完成,会呈现兼容性问题,在一些机型下可能会呈现键盘问题、闪耀或其它的功能开支,具体可看这篇介绍

数据同享

原生和 Flutter 运用不同的开发言语去开发,所以在一侧界说的数据结构目标和内存目标对方都无法感知,在数据同步和处理上有必要运用其它手段。

MethodChannel

Flutter 开发者对 MethodChannel 一定不陌生,开发当中免不了跟原生交互,MethodChannel 是双向规划,即答应咱们在 Flutter 中调用原生的办法,也答应咱们在原生中调用 Flutter 的办法。对 Channel 不太了解的能够看一下官方文档,如文档中提到的,这个通道传输的过程中需求将数据编解码,对应的联系以kotlin为例(完整的映射能够查看文档):

Dart                         | Kotlin      |
| -------------------------- | ----------- |
| null                       | null        |
| bool                       | Boolean     |
| int                        | Int         |
| int, if 32 bits not enough | Long        |
| double                     | Double      |
| String                     | String      |
| Uint8List                  | ByteArray   |
| Int32List                  | IntArray    |
| Int64List                  | LongArray   |
| Float32List                | FloatArray  |
| Float64List                | DoubleArray |
| List                       | List        |
| Map                        | HashMap     |

本地存储

这种办法比较简单理解,将本地存储视为中转站,Flutter中将数据操作存储到本地上,回到原生页面时在某个机遇(如onResume)去查询本地数据库即可,反之亦然。

问题

不管是MethodChannel或是本地存储,都会面临一个问题:目标的数据结构是独立的,两头需求重复界说。比方我在 Flutter 中有一个 Student 目标,Android 端也要界说一个相同结构的 Student,这样才能便利操作,现在我将Student student转成Unit8List传到Android,Channel中解码成Kotlin能操作的ByteArray,再将ByteArray转译成AndroidStudent目标。

class Student {
  String name;
  int age;
  Student(this.name, this.age);
}

对于这个问题最好的处理办法是运用DSL一类的结构,如Google的ProtoBuf,将同一份目标配置文件编译到不同的言语环境中,便能省去这部分双端重复界说的行为。

图片缓存

在内存方面,假如相同的图片在两头都加载时,会使得原生和 Flutter 都会发生一次缓存。在 Flutter 下默认就会缓存在ImageCache中,原生下不同的结构由不同的目标负责,为了去掉重复的图片缓存,势必要统一图片的加载办理。

阿里的计划也是如此,经过外接原生图片库,同享图片的本地文作缓存和内存缓存。它的完成思路是经过自界说ImageProviderCodec,对接外部图库,获取到图片数据做解析,对接的处理是经过扩展 Flutter Engine。

假如希望不修正Flutter Engine,也可经过外接纹路的办法去处理。经过PlatformChannel去恳求原生,使到图片的外接纹路数据,经过TextTure组件展现图片。

// 自界说 ImageProvider 中,经过 Channel 去恳求 textureId
var id = await _channel.invokeMethod('newTexture', {
  "imageUrl": imageUrl,
  "width": width ?? 0,
  "height": height ?? 0,
  "minWidth": constraints.minWidth,
  "minHeight": constraints.minHeight,
  "maxWidth": constraints.maxWidth,
  "maxHeight": constraints.maxHeight,
  "cacheKey": cacheKey,
  "fit": fit.index,
  "cacheOriginFile": cacheOriginFile,
});
// ImageWidget 中展现时经过 textureId 去显示图片
SizedBox(
  width: width,
  heigt: height,
  child: Texture(
    filterQuality: FilterQuality.high,
    textureId: _imageProvider.textureId.value,
  ),
)

总结

不同事务对于混合的程度和要求有所要求,并没有万能的计划。比方我团队的状况便是主Flutter混原生,在路由办理上我挑选了PlatformView这种处理形式,这种办法更简单开发和维护,后期假如发现有兼容性问题,也可过渡到flutter_boostFlutterEngineGroup上。