AndroidFlutter多实例实践
引⾔
Flutter CLI ⼯具⽀持将 Flutter Module 打包成 Android AAR 包以供外部依赖使⽤,即 Flutter AAR。在⼀个没有使⽤ Flutter 技术栈的 Android ⼯程中集成 Flutter AAR 是没有任何问题的,但如果⽬标⼯程本⾝已经使⽤了 Flutter 框架,在此基础上再接⼊ Flutter AAR 就会失败,我们称之为 Flutter 多实例问题。本⽂主要介绍在 Android 平台下 Flutter 多实例问题的⼀种解决⽅案。
背景
企业的业务往往是复杂多样的,如果是 ToC 的业务,我们⼤多时候需要开发⼀个体验良好的应⽤ APP;⽽如果是 ToB 的业务,我们往往需要提供⼀个易于接⼊和使⽤的 SDK。在 ToC 业务上,Flutter 框架提供的跨平台、⾼效开发与⾼性能特性,使得移动端应⽤开发变得更加简单且⾼效;那在 ToB 业务上,SDK 的开发是否能够享受 Flutter 框架提供的这些红利呢?这⼀点对于像我们⽹易云信这样的服务、能⼒提供商⽽⾔尤为重要。⽹易云信是集⽹易 21 年 IM 以及⾳视频技术打造的融合通信云服务专家,稳定易⽤的通信与视频 PaaS 平台,其服务⼤多以能⼒ SDK 的形式对外提供,如果能够提⾼ SDK 的⽣产效率和研发效能,好处不⾔⽽喻。所以,上⾯的问题答案当然是肯定的!就像使⽤ Flutter 开发 APP ⼀样,我们同样可以使⽤ Flutter 进⾏ SDK 开发,从⽽在 Android / iOS 甚⾄更多平台中共享⼀致
的业务逻辑实现,减⼩⼈⼒、提⾼⽣产效率和研发效能。
在使⽤ Flutter 进⾏ SDK 开发时,产物的打包⽅式主要有以下两种形式:
Flutter Package / Flutter Plugin:该打包⽅式需要以 Dart 源码形式发布到 Pub.dev 或 GitHub,第三⽅开发者在接⼊时本质上是以源码的形式依赖,同时接⼊⽅本地需要搭建并引⼊ Flutter 开发环境。此种⽅式有明显的缺陷:⾸先,源码发布会将 SDK 内部实现细节完全暴露在外( Flutter 框架并未提供类似 Proguard 的混淆⼯具),这对企业的⾮开源项⽬⽽⾔是不可接受的;其次,它变相要求接⼊⽅使⽤ Flutter 技术栈,这对于当前没有在⽬标项⽬中使⽤ Flutter 开发的接⼊⽅⽽⾔,门槛较⾼不说,接⼊体验也不太友好。
Android AAR:AAR 是 Android 应⽤官⽅的依赖形式,并不存在明显的短板。通过 Flutter 框架提供的 CLI ⼯具,可以很⽅便地将 Flutter Module 打包成 AAR 发布出去,不⽤担⼼泄漏业务源码,也不损失接⼊体验。因为打包⼯具会将 Flutter 层的业务代码编译成 AOT 共享库,⽽平台层的 Java 业务代码则可以开启混淆避免反编译(为了简便,后⾯统⼀使⽤ Flutter AAR 命名由 Flutter Module 打包⽽成的 Android AAR 包)。
综上所⾔,对于企业的⼀个商业 SDK 项⽬来说,如果选择使⽤ Flutter 技术栈进⾏开发,那么使⽤ Flutter AAR 形式来发布才是明智之举。但其实这⼜会引⼊新的问题。在前⽂中我们介绍了,⼀个 Flut
ter APP 的包结构,它包含有引擎库libflutter.so、业务库libapp.so、以及flutter_assets等部分。同理,⼀个 Flutter Module 打包出来的 AAR 也会包含类似的结构以及产物⽂件。那在⼀个 Flutter APP 中,应该以何种姿势接⼊ Flutter AAR 呢?可以预见的是,它们之间必然存在冲突,⽂件冲突已经显⽽易见,类、资源、甚⾄ Flutter Engine 也可能会冲突,这种常规的 Flutter AAR 包显然是⽆法集成到 Flutter APP ⼯程中使⽤的。有问题就有答案,接下来,我们就⼀起来分析、探索该问题的解决⽅案。Flutter APP 集成 Flutter AAR 问题分析
黄多多图书馆发生什么事上⾯说到 Flutter APP ⽆法集成常规打包出来的 Flutter AAR,因为存在⼀系列的冲突,但具体会出现什么样的错误,还是需要我们真正动⼿去集成才能知道。这个环节感兴趣的⼩伙伴可以亲⾃动⼿尝试,不再赘述,下⾯直接给出结论说明两者共存存在哪些问题:
构建失败,其实就是因为⽂件、类冲突导致编译失败。主要冲突有:
Flutter 版本依赖冲突:Flutter APP 宿主⼯程与 Flutter AAR 使⽤的 Flutter 版本不⼀致导致,包括 Flutter Embedding Jar 与 Flutter SO Jar,前者包含平台层 Java 代码,后者包含 libflutter.so 引擎库⽂件。通过 Gradle 我们可以解决这个依赖的版本冲突,例如强制使⽤其中某个版本,但这样做极有可能会出现运⾏时错误。
Flutter Plugin 平台代码 / 资源冲突:Flutter APP 和 Flutter AAR 引⽤了相同的 Plugin 但版本不⼀致导
致。插件中会包含平台层的代码,版本不⼀致同样可能会导致编译失败或者运⾏时错误。
GeneratedPluginRegistrant.java ⽂件冲突:该⽂件为 Flutter ⼯具⽣成的插件⾃动注册类,⽤于 Flutter Engine 启动时⾃动加载所需插件。Flutter APP 与 Flutter AAR 均有对应的类⽂件,负责加载各⾃依赖的插件,两者缺⼀不可。
libapp.so 冲突:这是 Dart 代码经过 AOT ⽣成的动态库,Flutter APP 和 Flutter AAR 都会⽣成与其对应的 so 库,我们不能单纯的只使⽤它们其中之⼀,因为它们本⾝包含的 AOT 代码是从不同的源码编译过来的。
运⾏时错误
同⼀个 Flutter Engine 不⽀持加载多个 AOT 库:Flutter Engine 在初始化时会动态链接 libapp.so 这个 AOT 库,解析其中的数据段,并执⾏代码段中的机器指令。但在我们的场景中,运⾏时其实是包含有两个 AOT 库的,它们都需要加载到 Flutter Engine 中来,使⽤同⼀个Engine 是⽆法满⾜需求的,因为在 Flutter 的实现中,⼀个 Engine 只能对应⼀个 AOT 库。优美的近义词
图⽚资源、字体库⽆法正常显⽰:此类资源会被打包⾄ flutter_assets 中,并且会⽣成对应的 Manifest 资源描述清单⽂件。但 Flutter APP ⽣成的资源清单⽂件会覆盖Flutter AAR 中的资源清单⽂件,这样导致 Flutter Engine 在加载资源时,⽆法从清单⽂件中查询到对应的资源,因此加载失败。
以上就是我们在 Flutter APP 中接⼊ Flutter AAR 遇到的问题。针对这些问题,我们⾸先想到的是,Flutter Team 或者开源社区是不是已经有此类问题的解决⽅案了?但在经过调研后发现⽬前并没有。Flutter 框架是⽀持多个 Engine 的,包括 Flutter 2.0 新⽀持的 Engine Group 仅⽀持加载和运⾏同⼀个 AOT 库下的代码,明显不能满⾜我们的需求。我们还给官⽅提了对应 Issue() 进⾏讨论,但是暂时还没有得到满意的解决⽅案,为此我们不得已⾛上了⾃⼰探索解决⽅案的⾃强之路。房祖明
解决⽅案探索
通过上⾯的分析,我们已经了解了接⼊过程中出现的具体错误以及出错原因。在真正着⼿探索解决⽅案前,还应设⽴⽬标解决⽅案应该满⾜的⼀些原则:
⾸先⽅案应该朝着最⼩引擎改动、甚⾄⽆改动的⽅向努⼒。因为 Flutter 框架⼀直在不断迭代演进,如果我们修改了引擎这块的逻辑,除⾮这些改动能通过 PR 进⼊主⼲分⽀,否则引擎⼀旦更新,我们的⽅案就得重新适配,后期维护⼯作⼤。
其次⽅案应该尽量不依赖宿主⼯程做额外的改造或⽀持。⾸先 Flutter APP 接⼊ Flutter AAR 就跟普通 Android APP 接⼊ Android AAR ⼀样简单,不应引⼊额外的插件或是Gradle 脚本;其次 Flutter AAR 和 Flutter APP 的 Flutter 运⾏时环境应该尽量隔离。
明确⽬标之后,我们再来看看⼊⼿点在哪⾥。由于需要尽量避免引擎改动,那应该是⾃上⽽下,⾸先从应⽤层切⼊,看能否到对策。这就需要我们深⼊源码,从上到下了解Flutter 框架的初始化、运⾏机制。这⾥不做单独讲解,在具体问题分析解决上再说明。现在我们再回过头来看最初遇到的⼀系列问题,并尝试运⽤所掌握的 Android 、Flutter 框架知识来解决。
Class 冲突解决
Class 冲突是因为 Flutter AAR 与 Flutter APP 都有⾃⼰的 Plugins 依赖、以及可能会依赖不同版本的 Flutter Embedding Jar,这些依赖库⾥都包含有平台代码,这会导致编译期类重复⽽失败。那如何解决这个问题呢?
最简单也是最暴⼒的⽅法就是对 Flutter AAR 依赖的所有 Plugin 以及 Embedding Jar 源码进⾏重命名(修改类名或者包名),虽然能解决问题,但⼯作量巨⼤、修改⾯⼴、不灵活,⼀旦 Plugin 或 Flutter 版本更新都需要重新修改。
那有没有更好的办法呢?答案是⾃定义ClassLoader。具体的,在构建 Flutter AAR 时,在源代码编译成 .class 阶段完成之后,将所有的插件、Flutter Embedding Jar 对应的
.class ⽂件搜集起来,打包成⼀个 DEX ⽂件放⼊ Flutter AAR 的 assets 中。在运⾏时,需要将 asset
新垣结衣男友s 下的 DEX ⽂件拷贝到应⽤的 data 私有⽬录下,再通过 DexClassLoader 去动态加载这个 DEX。这⾥需要注意的是 DEX ⽂件是版本号的概念的,它跟 Flutter AAR 的版本号是绑定的,意味着每次加载这个 DEX 时,我们⾸先需要检查当前私有⽬录下的⽂件版本是否与 Flutter AAR 版本⼀致,⼀致则直接加载即可,不⼀致需要删除原 DEX ⽂件并重新拷贝后再加载。关键代码如下:
针对 DEX ⽂件的加载⼀般⽽⾔我们只需要使⽤ DexClassLoader 这个系统类就⾏了,但这⾥我们需要继承 BaseDexClassLoader,并重写 findClass ⽅法。
默认类的加载基于双亲委派模型,⼀般都是先请求⽗加载器加载,如果⽗加载器加载失败⼦加载器才有机会加载。但在这⾥,我们 findClass 的逻辑需要反其道⽽⾏之。Flutter AAR 需要加载的类应该优先使⽤⼦加载器从 DEX ⽂件中加载,加载失败后才能通过⽗加载器加载。代码如下:
库⽂件冲突解决
libflutter.so 是Flutter Engine 动态库⽂件,在运⾏时会被 Flutter Embedder Jar 加载进来。这个库⽂件冲突,我们不能单纯使⽤宿主中同名的库⽂件,因为两者的 Engine 版本可能不⼀致以及不违背运⾏时 Flutter 版本隔离的⽬标。
这⾥解决冲突最简单的⽅法就是重命名。通过阅读代码,我们发现 Android 以 so 库的路径为 key 保存所有已经加载的动态库,即便是完全相同的 so 库,只要⽂件路径不⼀致,就可以同时 load 进来。因此,这⾥通过重命名能解决⽂件冲突的问题,也不会影响到 so 的加载。
libapp.so 冲突也是类似的,我们同样需要对 Flutter AAR 中的 libapp.so 重命名。此外,我们还需要特殊处理这两个 so 的加载流程。因为 Flutter 运⾏时硬编码了动态库的名称,如果不修改加载流程,在查考库时就会到 Flutter APP ⽣成的库⽂件,⽽不是我们 Flutter AAR 的库⽂件。
Flutter Engine 的初始化是在 FlutterLoader 这个类中,在这⾥会加载 libflutter.so 并配置⼀系列的参数初始化 Native Engine。我们需要做的就是替换 libflutter.so 的加载逻辑,转⽽去加载重命名后的 Engine 库⽂件。对于 libapp.so ,它并不是在 Java 层加载的,⽽是由 Native Engine 通过 dlopen 链接的。通过查阅 Engine 的代码我们发现通过 --aot-shared-library-name 选项可以设置要加载的⽬标 libapp.so 路径。关键代码如下:
Flutter 资源冲突解决
非常完美男嘉宾微博
Flutter 相关资源是打包放到 assets ⽬录下的,且通过对应的 Manifest ⽂件来声明,分别是:**FontM
anifest.json 与 AssetsManifest.json **⽂件。这两个⽂件分别列出了 Flutter 依赖的所有字体资源与路径映射关系、图⽚资源与路径映射关系。
Flutter-Engine 在运⾏时通过这两个⽂件来解析图⽚与字体资源,Flutter AAR 中虽然也包含了这两个⽂件,但会被 Flutter APP 宿主中的同名⽂件覆盖,导致字体或资源⽆法加载。所以,这⾥有两个简单⽅案:
⽀持编译期合并对应的资源清单 json ⽂件;这需要开发 Plugin 插件供宿主使⽤,实现复杂⽽且接⼊不友好;
Flutter AAR 中抽离出⼀个独⽴的资源包 Package 供 Flutter APP 依赖,资源包中仅包含 Flutter AAR 引⽤的所有图⽚、字体资源(不包含任何业务逻辑,因此可以放⼼的发布到pub平台),宿主在 Flutter 层依赖这个 Package,这样宿主在构建时 Flutter ⼯具会合并所有的的资源,并⽣成完整的资源清单⽂件。
⾄此,我们解决了 Flutter AAR 与 Flutter APP 的共存问题。当然整个⽅案落地下来,其中还会碰到其他⼀些问题,⽐如:⽣成的 DEX ⽂件需要访问宿主中的其他类的时候,在混淆启⽤的情况下,应该如何保证 DEX 访问主ClassLoader中的类、⽅法没有问题;再如:Flutter AAR 的 DEX 中如果包含有 Android 组件怎么办?Android 四⼤组件都是需要由应⽤的主ClassLoader进⾏加载的,如果主 DEX
中没有包含这些类,那么肯定启动失败;等等诸如此类问题,这⾥不再⼀⼀列举。
总结
下图所⽰为 Flutter 多实例运⾏时的架构图。类似于多 Flutter Engine,以上⽅案实现的多 Flutter 实例,也是通过创建多个 Native 的 AndroidShellHolder 来实现的。不同的是,在多 Engine 下不同的 ShellHolder 绑定相同的 libapp.so,⽽多实例下绑定的是不同的 libapp.so ,因此该⽅案能在运⾏时隔离 Flutter APP 与 Flutter AAR 的 Flutter 运⾏时环境。
该⽅案的主要优势表现在:
⽆ Engine 定制,可维护性较⾼
Flutter APP 与 Flutter AAR 的 Flutter 版本、运⾏时环境相互独⽴
有得必有失,相对地,在其他⽅⾯,该⽅案有所不⾜:
使⽤了独⽴的 Flutter Engine 库⽂件,因此会导致包体积增加
会加载两个不同的 Flutter Engine ,内存会有所增加
综上,在 SDK 开发中采⽤ Flutter 技术,同样能够发挥 Flutter 在 APP 开发中的优势,前提是我们能够解决好 Flutter 多实例的问题。本⽂主要讲解了 Android Flutter 多实例的⼀种实现思路,希望能够对⼤家有所帮助。散尾葵
作者简介
李成达,⽹易云信资深移动端开发⼯程师,热衷于研究跨平台开发技术以及⼯程提效,⽬前主要负责视频会议组件化 SDK 的相关研发⼯作。