本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
这是 《Flutter 工程化框架选择》 系列的第五篇 ,就像之前说的,这个系列只是单纯告诉你,创建一个 Flutter 工程,或者说搭建一个 Flutter 工程脚手架,应该如何快速选择适合自己的功能模块,或者说这是一个指引系列,所以比较适合新手同学。
这算是目前 Flutter 上少有关于 add-to-app 支持的指导分析了。
本来没想写这个话题,因为 add-to-app 混合开发一直是 Flutter 的痛,但目前又属于无法避免的场景 ,有时候甚至还有 RN内嵌 Flutter UI 这种“鬼畜”需求,所以既然大家都有这样的需要,那就来聊聊这类工程下有哪些选择。
首先为什么 add-to-app 在 Flutter 里很特殊?因为 Flutter 里的控件和路由等是通过独立的 FlutterEngine 进行绘制和管理,所以 Flutter 脱离会平台的 UI 渲染机制和页面堆栈。
也就是 Flutter 对于原生平台来说是一个“单页面”应用,举一个之前经常说的例子,如下图所示:
- 在当前 Flutter 端路由堆栈里有
FlutterA
和FlutterB
两个页面 Flutter 页面; - 这时候打开新的
Activity
/ViewController
,启动了原生页面X,可以看到原生页面 X 作为新的原生页面加入到原生层路由后,把默认的FlutterActivity
/FlutterViewController
给挡住,也就是把FlutterA
和FlutterB
都被挡住; - 这时候在 Flutter 层再打开新的
FlutterC
页面,可以看到依然会被原生页面X挡住;
所以通过这部分内容可以看出来,Flutter 默认情况下作为“单页面”应用,他们的路由堆栈是和原生层存在不兼容的隔离,其他问题还有如:内存数据互通、UI 控件嵌套等,这些都是 add-to-app 的痛点。
那关于 add-to-app 集成方式的选择上,官方在 Android 提供了两种集成方法,在 iOS 提供了三种集成方式,我个人推荐选择以下的集成方式:
-
Android 本地aar + 远程aar ,这种集成方式 Android 开发团队本地可以不需要安装 Flutter SDK
-
iOS 生成 xcframework ,这种集成方式 iOS 开发团队本地可以不需要安装 Flutter SDK
当然以上集成方式最大的问题就是需要分开调试,但从项目耦合上和协调开发上我个人觉得会更符合要求。
另外,如果你对如何使用 add-to-app 还有疑问,那官方的 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
可能对部分人来说比较陌生,它是在 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 例子里也有在一个页面内放置两个 FlutterFragment
的实现,重点还是在于 FlutterEngineGroup
可以在多 Engine 混合模式下保持极低的内存占用。
之所以 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
共享数据
接入 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 add-to-app 这个领域里, flutter_boost 相信大家肯定不会陌生,作为最早开源并且支持混合开发路由的框架,flutter_boost 采用的是单 Engine 内存共享的方式 。
这种实现方式的好处很明显:那就是 Dart 层面数据状态支持共享,因为只有一个 Engine ,但是问题也很明显,如下图是 flutter_boost 在 2.0 时的渲染流程,维护一个 Engine 渲染多个 Surface 这种非官方实让 flutter_boost 在很长一段时间没能快速推进项目。
不过 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 实现,最大问题就是可能遇到渲染切换上的时机问题,例如:
可以看到在 Flutter 的实现机制上维护一套机制外的逻辑缺失不容易,更不容易的是 flutter_boost 现在还在积极推进和维护,从目前来看 flutter_boost 3.0 会是一个不错的选择。
flutter_thrio 一开始是哈罗单车开源的 add-to-app 集成方案,后来哈罗不再维护之后,由 flutter_thrio 组织社区开源进行维护。
flutter_thrio 采用多 Engine 模式,同时又支持 Engine 复用的逻辑,从目前的代码(bf16529)上看,大概逻辑就是:
所有路由操作都通过原生端支持维护,而一个
ThrioFlutterActivity
/FlutterViewController
可以承载多个 Dart 页面,如果 last 页面不是 Flutter 容器,就spawn
一个新的FlutterEngine
并构建新的 Flutter 容器页面。
flutter_thrio 的内部实现的多 Engine 模式和 FlutterEngineGroup
那套基本一样,不过它是通过反射拿到 FlutterEngine
里的 flutterJNI
,然后通过 spawn
构建新的 FlutterEngine
。
PS,目前 flutter_thrio 里的
isMultiEngineEnabled
标识位感觉有些具备迷惑性,其实它更多是控制entrypoint
,通过entrypoint
来决定是否启动新的FlutterEngine
容器。
另外 flutter_thrio 通过封装,在三端利用统一的 notify
接口来实现页面通知进行数据同步,这也是目前 add-to-app 里数据联通的常规实现方案。
那 flutter_thrio 有什么优势?
FlutterEngine
可以按需启动- 更低的内存占用
- 统一的路由堆栈和数据接口
- 轻量级代码,更简便接入
那这个库有什么问题?
- 不兼容
Fragment
级别支持 - 不提供 iOS 中存在的
present
功能 - 官方不再维护,社区库维护投入有限,理解项目迭代纯靠源码:”不打算好好看看源码的使用者可以放弃这个库了,因为很多设定是比较死的,而我本人不打算花时间写太多文档“
flutter_thrio 很多 Feat 和想法还是很不错的,只是维护的资源有限,希望未来项目还能够继续持续推进。
mix_stack 是个人开源的混合路由库,从设计上看它类似于早期更轻量级的 boost ,采用的是单 Engine 模式,所以也是通过跳转 Flutter 容器时切换 Surface 来实现路由混合。
在 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
。
如上图右侧动态,在页面内通过 NativeOverlayReplacer
指定 autoHidesOverlayNames
,然后在弹出 Flutter popup 路由之前调用 registerAutoPushHiding
,最终就可以实现 Flutter 控件“渲染在 Native” 之上的效果。
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 采用的是 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 运行的情况下内存占用问题也还可以。
fusion 的优势其实在于作者对一些细节处理比较上心,比如:
- Flutter 容器与 Native 容器跳转时状态栏图标颜色可能出现显示不正确的问题;
- 混合开发时当栈顶是 Flutter 页面时进入到任务界面其应用名称不显示的问题;
- 混合开发时 Android APP 在后台被系统回收后再次进入 Flutter 页面不能恢复的问题
- 支持生命周期调用
当然 fusion 的劣势也很明显:
- 作者 gtbluesky 好像并没有在 Github 开源,所以目前交流反馈存在问题
- 存在某些场景下闪动问题
- 暂不知道是否还有什么坑
PS:如果 demo 跑不起来,先把 compileSdkVersion 改为 32 ,所有 ext.kotlin_version 改为 '1.7.10' 就可以了
通过上面分享关于 add-to-app 的现状和框架支持,相信大家对于相关的实现应该都有了一定的了解,采用什么方案和框架具体还是取决于你的需求场景,不管是哪个框架目前都有坑和局限,重点还是在于它未来是否持续维护,或者不维护了我们自己能否继续维护下去。
这里说一个题外话,其实开源更多是提供解决思路,有效的沟通和 PR 才能推进项目的健康发展,如果社区内基本都是一味的 issue 等待解决,那基本项目都很难长久,所以项目是不是 KPI 并不重要,重要的是它提供的思路是否有用,这才是我认为的开源里最大的价值。