GSYFlutterBook/Z10.md

267 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 并不重要,重要的是它提供的思路是否有用,这才是我认为的开源里最大的价值。