GSYFlutterBook/Z10.md

267 lines
18 KiB
Markdown
Raw Permalink Normal View History

2023-01-04 09:38:40 +08:00
# 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 并不重要,重要的是它提供的思路是否有用,这才是我认为的开源里最大的价值。