# Flutter 工程化框架选择 — add-to-app 的指路明灯 ``` 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! ``` 这是 《Flutter 工程化框架选择》 系列的第五篇 ,就像之前说的,这个系列只是单纯告诉你,创建一个 Flutter 工程,或者说搭建一个 Flutter 工程脚手架,应该如何快速选择适合自己的功能模块,或者说这是一个指引系列,所以比较适合新手同学。 > **这算是目前 Flutter 上少有关于 add-to-app 支持的指导分析了**。 本来没想写这个话题,因为 add-to-app 混合开发一直是 Flutter 的痛,但目前又属于无法避免的场景 ,有时候甚至还有 [RN内嵌 Flutter UI ](https://cloud.tencent.com/developer/article/1896484)这种“鬼畜”需求,**所以既然大家都有这样的需要,那就来聊聊这类工程下有哪些选择**。 | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image1.png) | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image2.png) | | ------------------------------------------------------------ | ------------------------------------------------------------ | # add-to-app 首先为什么 add-to-app 在 Flutter 里很特殊?**因为 Flutter 里的控件和路由等是通过独立的 FlutterEngine 进行绘制和管理,所以 Flutter 脱离会平台的 UI 渲染机制和页面堆栈**。 也就是 Flutter 对于原生平台来说是一个“单页面”应用,举一个之前经常说的例子,如下图所示: - 在当前 Flutter 端路由堆栈里有 `FlutterA` 和 `FlutterB` 两个页面 Flutter 页面; - 这时候打开新的 `Activity` / `ViewController`,启动了**原生页面X**,可以看到**原生页面 X** 作为新的原生页面加入到原生层路由后,把默认的 `FlutterActivity` / `FlutterViewController` 给挡住,也就是把 `FlutterA` 和 `FlutterB` 都被挡住; - **这时候在 Flutter 层再打开新的 `FlutterC` 页面,可以看到依然会被原生页面X挡住**; ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image3.png) 所以通过这部分内容可以看出来,**Flutter 默认情况下作为“单页面”应用,他们的路由堆栈是和原生层存在不兼容的隔离**,其他问题还有如:内存数据互通、UI 控件嵌套等,这些都是 add-to-app 的痛点。 那关于 add-to-app 集成方式的选择上,官方在 Android 提供了两种集成方法,在 iOS 提供了三种集成方式,我个人推荐选择以下的集成方式: - **Android [本地aar + 远程aar](https://docs.flutter.dev/development/add-to-app/android/project-setup#option-a---depend-on-the-android-archive-aar)** ,这种集成方式 Android 开发团队本地可以不需要安装 Flutter SDK - **iOS [生成 xcframework ](https://docs.flutter.dev/development/add-to-app/ios/project-setup#option-b---embed-frameworks-in-xcode)**,这种集成方式 iOS 开发团队本地可以不需要安装 Flutter SDK 当然以上集成方式最大的问题就是需要分开调试,但从项目耦合上和协调开发上我个人觉得会更符合要求。 **另外,如果你对如何使用 add-to-app 还有疑问,那官方的 [put-flutter-to-work](https://github.com/flutter/put-flutter-to-work) 项目就是一个很好的参考例子。** > add-to-app 跑不起来?参考这个 Demo 是你的最佳选择,例如配置依赖和通过 `FlutterEngineCache` 和 `executeDartEntrypoint` 预热 Engine 等。 # 混合路由 这是本篇的重点,通过前面简单的例子,我们可以预见在 add-to-app 里混合路由和数据共享将会是最大的障碍,例如: - 打开一个原生页面 A - 再打开一个 Flutter 页面 B - 再打开一个原生页面 C - 再打开一个 Flutter 页面 D 而这个过程中既要保证混合路由的同步,又要保证数据交互的畅通,那么在这个基础上,**统一路由到原生页面堆栈, “多开 Flutter 页面” 就成为必然的需求**,但是对“多开”的实现又有不同的选择: - 每个 Flutter 页面新建一个 Flutter Engine - 多个 Flutter 页面共享一个 Flutter Engine **不同实现各自的利弊,而本篇也是主要介绍它们的实现者各自的利弊**。 ## FlutterEngineGroup `FlutterEngineGroup` 可能对部分人来说比较陌生,它是在 Flutter 2.0 时发布的 add-to-app 的官方解决方案,**`FlutterEngineGroup` 方案使用了多 Engine 混合支持,官方宣称除了一个 Engine 对象之外,后续每个 Engine 对象在 Android 和 iOS 上仅占用 180kB** 。 > 以前的方案每多一个Engine ,可能就会多出 19MB Android 和 13MB iOS 的占用。 在使用 `FlutterEngineGroup` 之后,`FlutterEngine` 都将由 `FlutterEngineGroup` 去生成,生成的 `FlutterEngine` 可以独立应用于 `FlutterActivity`/`FlutterViewController`、 `FlutterFragment` 和 `FlutterView` 等。 其实 `FlutterEngineGroup` 不一定是用于混合路由,如下动图所示,在官方的 [multiple_flutters](https://github.com/flutter/samples/tree/main/add_to_app/multiple_flutters) 例子里也有在一个页面内放置两个 `FlutterFragment` 的实现,**重点还是在于 `FlutterEngineGroup` 可以在多 Engine 混合模式下保持极低的内存占用**。 | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image4.gif) | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image5.png) | | ------------------------------------------- | ------------------------------------------------------------ | 之所以 `FlutterEngineGroup` 能在多 Engine 模式下保持极低的内存占用, **其实得益于通过 `FlutterEngineGroup` 生成的 `FlutterEngine` 可以共享 GPU 上下文, font metrics 和 isolate group snapshot** ,也就是新 Engine 可以通过旧 Engine `spawn` 出来。 而 `FlutterEngineGroup` 里主要是通过 `dartEntrypoint` 来制定入口: - **`findAppBundlePath` 默认指向的 `flutter_assets` 目录**; - **`entrypoint` 其实就是 dart 代码里启动方法的名称**;也就是绑定了在 dart 中 `runApp` 的方法,在 dart 可以通过 `@pragma('vm:entry-point')` 来指定 - dart 层和原生层通过 `MethodChannel` 共享数据 | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image6.png) | | ------------------------------------------------------------ | | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image7.png) | | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image8.png) | 接入 `FlutterEngineGroup` 之后: - 在 dart 层面可以通过 `MethodChannel` 打开原始页面; - 在原生层可以通过新建 `FlutterEngine` 打开新的 Flutter 页面; - 甚至你还可以在原生层打开一个 `FlutterView` 的 Dialog; 当然,到这里你可能已经注意到了,因为每个 Flutter 页面都是一个独立的 Engine ,由于 dart isolate 的设计理念,**每个独立 Engine 的 Flutter 页面内存是无法共享的**。 也就是说,当你需要共享数据时,只能在原生层持有数据,然后注入或者传递到每个 Flutter 页面中,就像官方所说的,**每个 Flutter 页面更像是一个独立 Flutter 模块**。 > 当然这也造成了一些不必要的麻烦,比如:**同一张图片,在原生层、不同 Flutter Engine 会出现多次加载的问题**,这种问题可能就需要你针对 Flutter 的图片加载使用外界纹理,来实现在原生层统一的内存管理等。 **而 `FlutterEngineGroup` 的好处也很直观:官方维护,不需要第三方框架,轻量级**。 其实 FlutterEngineGroup 不只是在多页面下的场景有价值,就算你只有一个 Engine 也可以考虑它,例如当你在 Service 里去创建 Flutter Engine 并构建独立的 `FlutterView` 效果,但是页面在静止 20 分钟左右后就可能会出现: > E/MessageQueue-JNI: java.lang.RuntimeException: Cannot execute operation because FlutterJNI is not attached to native. 这个时候如果将 Engine 的创建方式换成 FlutterEngineGroup ,你会发现 Engine 因为多次创建和被回收的问题将得到极大程度的缓解。 > **这里为什么要介绍那么长的 `FlutterEngineGroup` ,因为它是后面的框架有很大关系。** ## flutter_boost 在 Flutter add-to-app 这个领域里, [flutter_boost ](https://github.com/alibaba/flutter_boost)相信大家肯定不会陌生,作为最早开源并且支持混合开发路由的框架,**flutter_boost 采用的是单 Engine 内存共享的方式** 。 ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image9.png) 这种实现方式的好处很明显:**那就是 Dart 层面数据状态支持共享**,因为只有一个 Engine ,但是问题也很明显,如下图是 flutter_boost 在 2.0 时的渲染流程,维护一个 Engine 渲染多个 Surface 这种非官方实让 flutter_boost 在很长一段时间没能快速推进项目。 ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image10.png) 不过 flutter_boost 几乎是早期 add-to-app 的不二之选,**但如果你现在要使用 flutter_boost, 那么最好你的 Flutter SDK 是从 3.0 开始**。 因为在 3.0 之前 flutter_boost 自己拷贝并维护了一套 Engine Embedding 层的自定义代码,这部分代码导致 flutter_boost 更新速度慢并且入侵性更强,而 **flutter_boost 3.0 开始采用继承的方式扩展,后期兼容性更好**。 > 虽然 flutter_boost 也表示 flutter_boost 3.0 会兼容 Flutter 2.0 ,但是你懂的。 从目前的 flutter_boost 3.0 上来看,新版本优势主要有: - 和 flutter_boost 2.0 对比 Android 和 iOS 两端 API 得到对齐,统一生命周期,代码优化后更好阅读 - 不侵入 Engine 代码,兼容更多 Flutter 版本,避免因为 flutter_boost 而导致的无法升级 Flutter SDK 等问题 - 目前在仍在维护 当然,如果是对比 `FlutterEngineGroup` flutter_boost 的优势在于: - 支持页面间数据传递 - 统一的混合路由调用接口 - Dart 层面数据支持共享 - 支持跨端事件传递 那缺陷可能是什么?flutter_boost 采用的单 Engine 实现,最大问题就是可能遇到渲染切换上的时机问题,例如: - [动画区域会有不停地闪烁出现](https://github.com/alibaba/flutter_boost/issues/1740) - [后台停留出现假死](https://github.com/alibaba/flutter_boost/issues/1671) - [路由跳转或返回可能出现白屏](https://github.com/alibaba/flutter_boost/issues/1719) ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image11.gif) 可以看到在 Flutter 的实现机制上维护一套机制外的逻辑缺失不容易,更不容易的是 flutter_boost 现在还在积极推进和维护,从目前来看 flutter_boost 3.0 会是一个不错的选择。 ## flutter_thrio [flutter_thrio](https://github.com/flutter-thrio/flutter_thrio) 一开始是哈罗单车开源的 add-to-app 集成方案,**后来哈罗不再维护之后,由 flutter_thrio 组织社区开源进行维护**。 flutter_thrio 采用多 Engine 模式,同时又支持 Engine 复用的逻辑,从目前的代码([bf16529](https://github.com/flutter-thrio/flutter_thrio/commit/2e0f53861a7c91ba13c0b2c262e4733dfbf16529))上看,大概逻辑就是: > 所有路由操作都通过原生端支持维护,而一个 `ThrioFlutterActivity` / `FlutterViewController` 可以承载多个 Dart 页面,如果 last 页面不是 Flutter 容器,就 `spawn` 一个新的 `FlutterEngine` 并构建新的 Flutter 容器页面。 ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image12.png) flutter_thrio 的内部实现的多 Engine 模式和 `FlutterEngineGroup` 那套基本一样,不过它是通过反射拿到 `FlutterEngine` 里的 `flutterJNI` ,然后通过 `spawn` 构建新的 `FlutterEngine` 。 > PS,目前 flutter_thrio 里的 `isMultiEngineEnabled `标识位感觉有些具备迷惑性,其实它更多是控制 `entrypoint` ,通过 `entrypoint` 来决定是否启动新的 `FlutterEngine` 容器。 ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image13.png) 另外 flutter_thrio 通过封装,在三端利用统一的 `notify` 接口来实现页面通知进行数据同步,这也是目前 add-to-app 里数据联通的常规实现方案。 那 flutter_thrio 有什么优势? - `FlutterEngine` 可以按需启动 - 更低的内存占用 - 统一的路由堆栈和数据接口 - 轻量级代码,更简便接入 那这个库有什么问题? - 不兼容 `Fragment` 级别支持 - 不提供 iOS 中存在的 `present` 功能 - 官方不再维护,社区库维护投入有限,理解项目迭代纯靠源码:”**不打算好好看看源码的使用者可以放弃这个库了,因为很多设定是比较死的,而我本人不打算花时间写太多文档**“ > flutter_thrio 很多 [Feat ](https://github.com/flutter-thrio/flutter_thrio/issues/4)和想法还是很不错的,只是维护的资源有限,希望未来项目还能够继续持续推进。 ## mix_stack [mix_stack](https://github.com/yuewen/mix_stack) 是个人开源的混合路由库,从设计上看它类似于早期更轻量级的 boost ,采用的是单 Engine 模式,所以也是通过跳转 Flutter 容器时切换 Surface 来实现路由混合。 ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image14.png) 在 mix_stack 里每一个 Native Flutter Container 都会包含了一个独立的 Navigator 用于维持 Flutter 内栈管理,通过 Flutter 内 Stack 控制当前渲染 Native Container 所属的 Flutter 页面堆栈。 而 mix_stack 的特点就是**支持多 Tab Flutter View 和 Flutter 端控制 Native 界面隐藏显示** ,其中最有意思的就是 Flutter 端控制 Native 界面隐藏显示的 `NativeOverlayReplacer` 。 举个例子,如下图所示,此时 `FlutterView` 上面有两个 Native 的控件 navigationBar 和 tabBar ,如果此时我们直接弹出新的 Flutter Route 肯定会被这两个 Native 控件遮挡,但是因为需要 `Hero` 效果,所以又不希望用新的原生容器去承载,这时候就可以用 `NativeOverlayReplacer` 。 | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image15.png) | ![](http://img.cdn.guoshuyu.cn/20221101_Z10/image16.gif) | | ------------------------------------------------------------ | ------------------------------------------- | 如上图右侧动态,在页面内通过 `NativeOverlayReplacer` 指定 `autoHidesOverlayNames` ,然后在弹出 Flutter popup 路由之前调用 `registerAutoPushHiding` ,最终就可以实现 Flutter 控件“渲染在 Native” 之上的效果。 ```dart NativeOverlayReplacer( autoHidesOverlayNames: ["tabBar", "navigationBar"], ···· NativeOverlayReplacer.of(context).registerAutoPushHiding ``` 这里面的原理其实是在页面打开时,通过在原生层利用 `createBitmap` 将两个 Native 控件进行截图,并将 bitmap `toByteArray` 传递到 Flutter 层,之后只需要在 Flutter 控件展示时对原生控件进行隐藏,并通过 `Stack` 在相应位置绘制出 native 控件的 Bitmap ,就可以达到类似 Flutter 控件“渲染在 Native” 之上的效果。 所以针对 mix_stack 的优势在于: - 支持 View 级别调用 - 更加灵活 - 拥有 `NativeOverlayReplacer` 特性 - 支持多 Tab Flutter View mix_stack 的劣势也很明显: - mix_stack 相对入侵性较强, 例如它在 android 上通过各种反射获取 `FlutterView` 里的 `FlutterEngine` 、 `FlutterTextureView` 等 ,同时还利用反射获取了 `embedding` 的各种内部变量和方法,这对持续维护和稳定性有一定的影响 - 个人维护,目前用户不多,可踩坑程度未知。 > PS:如果你跑 Demo 发现 Android Install Failed ,可以将 `android/app` 目录下的 `debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'` 屏蔽 ## fusion [fusion](https://pub.dev/packages/fusion) 采用的是 `FlutterEngineGroup` 方案,默认在 Flutter 与 Native 页面多次跳转情况下,APP 始终仅有一份 FlutterEngine 实例,也就是对应 fusion 里的 `REUSE_MODE` 。 > 当然, fusion 针对 `FusionFragment` 和 `Dialog` 等场景也提供了不复用场景,可以通过 `FlutterEngineGroup` 底成本构建独立 Engine 的支持。 如果从代码层面看,fusion 代码相对会简洁不少,比如 `FusionActivity` 在继承 `FlutterActivity` 之后,主要做的两件事: - 找到当前 activity 下的 `FlutterView` ,调用 `detachFromFlutterEngine` 停用 - 在 `onResume` 里调用 `engine.activityControlSurface.attachToActivity` 和 `flutterView?.attachToFlutterEngine` 复用引擎。 fusion 的设计理念就是尽可能简洁地去融合对应逻辑,所以侵入性不高,能复用 `embedding ` 相关的逻辑就不自定义,从目前 Demo 运行的情况下内存占用问题也还可以。![](http://img.cdn.guoshuyu.cn/20221101_Z10/image17.png) fusion 的优势其实在于作者对一些细节处理比较上心,比如: - Flutter 容器与 Native 容器跳转时状态栏图标颜色可能出现显示不正确的问题; - 混合开发时当栈顶是 Flutter 页面时进入到任务界面其应用名称不显示的问题; - 混合开发时 Android APP 在后台被系统回收后再次进入 Flutter 页面不能恢复的问题 - 支持生命周期调用 当然 fusion 的劣势也很明显: - 作者 [gtbluesky ]( https://github.com/gtbluesky/fusion) 好像并没有在 Github 开源,所以目前交流反馈存在问题 - 存在某些场景下闪动问题 - 暂不知道是否还有什么坑 > PS:如果 demo 跑不起来,先把 compileSdkVersion 改为 32 ,所有 ext.kotlin_version 改为 '1.7.10' 就可以了 # 最后 通过上面分享关于 add-to-app 的现状和框架支持,相信大家对于相关的实现应该都有了一定的了解,采用什么方案和框架具体还是取决于你的需求场景,不管是哪个框架目前都有坑和局限,重点还是在于它未来是否持续维护,或者不维护了我们自己能否继续维护下去。 这里说一个题外话,其实开源更多是提供解决思路,有效的沟通和 PR 才能推进项目的健康发展,如果社区内基本都是一味的 issue 等待解决,那基本项目都很难长久,所以项目是不是 KPI 并不重要,重要的是它提供的思路是否有用,这才是我认为的开源里最大的价值。