Flutter 工程化框架选择 — add-to-app 的指路明灯

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

这是 《Flutter 工程化框架选择》 系列的第五篇 ,就像之前说的,这个系列只是单纯告诉你,创建一个 Flutter 工程,或者说搭建一个 Flutter 工程脚手架,应该如何快速选择适合自己的功能模块,或者说这是一个指引系列,所以比较适合新手同学。

这算是目前 Flutter 上少有关于 add-to-app 支持的指导分析了

本来没想写这个话题,因为 add-to-app 混合开发一直是 Flutter 的痛,但目前又属于无法避免的场景 ,有时候甚至还有 RN内嵌 Flutter UI 这种“鬼畜”需求,所以既然大家都有这样的需要,那就来聊聊这类工程下有哪些选择

add-to-app

首先为什么 add-to-app 在 Flutter 里很特殊?因为 Flutter 里的控件和路由等是通过独立的 FlutterEngine 进行绘制和管理,所以 Flutter 脱离会平台的 UI 渲染机制和页面堆栈

也就是 Flutter 对于原生平台来说是一个“单页面”应用,举一个之前经常说的例子,如下图所示:

  • 在当前 Flutter 端路由堆栈里有 FlutterAFlutterB 两个页面 Flutter 页面;
  • 这时候打开新的 Activity / ViewController,启动了原生页面X,可以看到原生页面 X 作为新的原生页面加入到原生层路由后,把默认的 FlutterActivity / FlutterViewController 给挡住,也就是把 FlutterAFlutterB 都被挡住;
  • 这时候在 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 是你的最佳选择,例如配置依赖和通过 FlutterEngineCacheexecuteDartEntrypoint 预热 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/FlutterViewControllerFlutterFragmentFlutterView 等。

其实 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_boost

在 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

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

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 里的 FlutterEngineFlutterTextureView 等 ,同时还利用反射获取了 embedding 的各种内部变量和方法,这对持续维护和稳定性有一定的影响

  • 个人维护,目前用户不多,可踩坑程度未知。

PS:如果你跑 Demo 发现 Android Install Failed ,可以将 android/app 目录下的 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' 屏蔽

fusion

fusion 采用的是 FlutterEngineGroup 方案,默认在 Flutter 与 Native 页面多次跳转情况下,APP 始终仅有一份 FlutterEngine 实例,也就是对应 fusion 里的 REUSE_MODE

当然, fusion 针对 FusionFragmentDialog 等场景也提供了不复用场景,可以通过 FlutterEngineGroup 底成本构建独立 Engine 的支持。

如果从代码层面看,fusion 代码相对会简洁不少,比如 FusionActivity 在继承 FlutterActivity 之后,主要做的两件事:

  • 找到当前 activity 下的 FlutterView ,调用 detachFromFlutterEngine 停用
  • onResume 里调用 engine.activityControlSurface.attachToActivityflutterView?.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 并不重要,重要的是它提供的思路是否有用,这才是我认为的开源里最大的价值。

results matching ""

    No results matching ""