Flutter 混合开发 - 动态下发 libflutter.so libapp.so

背景

最近在做包体积优化,在完成代码混淆、压缩,裁剪ndk支持架构,以及资源压缩(如图片转webp、mp3压缩等)后发现安装包的中占比较大的仍是 so 动态库依赖。
image.png具体查看发现 libflutter.so 和 libapp.so 的体积是最大的,这两个动态库都是 flutter 集成进来的。image.png结合项目中 Flutter 的应用,Flutter 页面都是作为二级页面使用,而且页面使用频率很低,所以是不是可以把这两个 so 从 apk 中剔除,在应用启动后再动态下发呢?
如果可以实现,那么包体积又可以缩减 13.8 M,包体积在原基础上立减一半,收益非常可观!开搞!

实战

libflutter.so & libapp.so 如何引入项目的?

项目是以远程依赖方式引入 flutter,即 flutter 开发完成后打包 aar 发布到公司 maven。通过解压已打包的 aar 发现,aar 中仅有 libapp.so,并没有 libflutter.so。而唯一提到 libflutter.so 的只有打包时生成的 pom 文件。
image.png
那么就从宿主项目入手。要远程依赖 flutter,需要指定 repositories{} 。通过配置发现,除了公司 maven 仓库地址,还需要额外配置一个 "https://storage.flutter-io.cn/download.flutter.io",结合打包时生成的 pom 文件,可以猜测 libflutter.so 是在依赖解析过程中引入到项目中的。

allprojects {repositories {google()mavenCentral()//flutter 需要的仓库配置:maven {url '******'  //公司 maven 仓库地址}maven {url 'https://storage.flutter-io.cn/download.flutter.io'}}
}

如何剔除与上传 libflutter.so & libapp.so

知道了这两个 so 文件如何引入到项目中的,那么接下来就要考虑怎么剔除与上传。剔除的时机有两个时间节点:打包 aar 时,打包 apk 时。结合已了解的 so 文件引入时机,打包 aar 时只能剔除 libapp.so,显然这个时机不合适,那么下面就来看打包 apk 时怎么实现剔除并上传这两个 so 文件。
既然要在打包 apk 时剔除并上传,毫无疑问需要自定义 Gradle Plugin 和 Gradle Task。如何自定义不细讲,网上相关文章太多,自行查看。

这里考虑只在项目中使用,所以直接在项目中新建 buildSrc Module,在里面实现 Gradle Plugin。

自定义 Gradle Plugin

  1. 明确只在打 release 包时才需要剔除(因为谁关心 debug 包包体积呀!)
  2. 确定剔除 Task 执行的时机。剔除要在 merge 所有 so 之后才行,通过查看 task 列表,发现 “mergeReleaseNativeLibs” 就是非常不错的时机。
public class FlutterDynamicPlugin implements Plugin<Project> {@Overridepublic void apply(Project project) {if (project.getPlugins().hasPlugin("com.android.application")) {project.afterEvaluate(project1 -> {AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);appExtension.getApplicationVariants().all(variant -> {String variantName = StringUtil.capitalize(variant.getName());//只在 release 变体下生效if (!variantName.equalsIgnoreCase("release")) return;//自定义 Gradle TaskEngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);//指定自定义 Task 执行时机:mergeReleaseNativeLibs -> flutterSoDynamicReleaseTask mergeSOTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");mergeSOTask.finalizedBy(engineSoDynamicTask);});});}}
}

自定义 Gradle Task

  1. 找到 libflutter.so
  2. 上传
  3. 剔除
  4. 记录上传信息(用于运行时下载)

public class EngineSoDynamicTask extends DefaultTask {@Inputpublic String mergeNativeLibsOutputPath;@TaskActionpublic void optimizeEngineSo() {//从 app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a 中找到 libflutter.soFile soFile = FileUtil.findSpecificFile(mergeNativeLibsOutputPath, "arm64-v8a", "libflutter.so");if (soFile == null || !soFile.exists()) return;//上传String url = HttpUtil.getInstance().upload(soFile);if (url != null){//记录上传信息write2Assets(url);//剔除soFile.delete();}}private void write2Assets(String url) {String content = "\"flutterSoUrl\":\"" + url + "\"";Write2AssetsUtil.getInstance().writeContent(content);}
}

这里以剔除 libflutter.so 为例,由于项目中只支持 arm64-v8a,所以只剔除了该架构下的。

坑点: 记录上传信息是通过向 assets 中插入 json 文件实现的,而上面只指定了自定义 Task 在 mergeReleaseNativeLibs Task 之后执行,这里就会偶现 assets 插入成功了,但打出的 apk 的 asstes 中并没有 json 文件。

原因: mergeReleaseNativeLibs Task 与 mergeReleaseAssets Task 没有指定的先后顺序,这就导致 assets 插入成功了,但被后续的 mergeReleaseAssets Task 覆盖掉了。

解决办法: 指定自定义 Task 、mergeReleaseNativeLibs Task、mergeReleaseAssets Task 三者先后顺序

EngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);
Task mergeNativeLibsTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");
Task mergeAssetsTask = project.getTasks().findByName("merge" + variantName + "Assets");                    // mergeReleaseNativeLibs -> flutterSoDynamicRelease -> mergeReleaseAssets
mergeNativeLibsTask.finalizedBy(engineSoDynamicTask);
mergeAssetsTask.dependsOn(engineSoDynamicTask);

运行时动态加载

libflutter.so & libapp.so 使用时机

要实现动态加载,先明确这两个 so 文件在何时用到,找到这个时间点,只要在其之前下载完成就,理论上就实现了运行时动态加载。
项目中使用的是官方多引擎方案(即 EngineGroup),所以先看它的构造函数中有何逻辑。

public class FlutterEngineGroup {public FlutterEngineGroup(@NonNull Context context) {this(context, null);}public FlutterEngineGroup(@NonNull Context context, @Nullable String[] dartVmArgs) {// FlutterInjector.instance() 该方法会创建一个 FlutterInjector 单例,//   FlutterInjector 实例创建过程中会创建 FlutterLoader 对象并赋值给 flutterLoader 变量FlutterLoader loader = FlutterInjector.instance().flutterLoader();if (!loader.initialized()) {loader.startInitialization(context.getApplicationContext());loader.ensureInitializationComplete(context.getApplicationContext(), dartVmArgs);}}
}

FlutterEngineGroup 构造函数中直接创建获取 FlutterLoader 对象,然后调用其 startInitialization() 和 ensureInitializationComplete()。限于篇幅,这里直接说结论:

  • startInitialization() 最终会执行 FlutterJNI#loadLibrary(),其内部调用 System.loadLibrary(“flutter”),实现加载 libflutter.so。
  • ensureInitializationComplete() 内部会准备一个 shellArgs 配置,最终调用 FlutterJNI#init() 执行。shellArgs 中有两条是关于 libapp.so 的。
public void ensureInitializationComplete({//...List<String> shellArgs = new ArrayList<>();//...shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);shellArgs.add("--"+ AOT_SHARED_LIBRARY_NAME+ "="+ flutterApplicationInfo.nativeLibraryDir+ File.separator+ flutterApplicationInfo.aotSharedLibraryName);//...
}

通过上面可知,libflutter.so 和 libapp.so 都是在 FlutterEngineGroup 构造时调用的,那么只要在 FlutterEngineGroup 构造之前下载完成即可。

动态加载 libflutter.so

查看 FlutterEngineGroup 构造函数源码可知,libflutter.so 是通过 System.loadLibrary(“flutter”) 来实现加载的。结合 so 加载流程可知,将自定义的 so 文件路径注入到 classLoader#pathList#nativeLibraryDirectories 就可以实现优先加载,就可以实现 so 的动态加载了。这里我们直接复用 Tinker 的 TinkerLoadLibrary#installNativeLibraryPath() 。

动态加载 libapp.so

查看 FlutterEngineGroup 构造函数源码可知,libapp.so 是添加到一个配置中,然后调用 native 方法执行,所以无法想 libflutter.so 来实现。首先能想到的是能不能 hook 方法来自己实现配置,再次查看 FlutterEngineGroup 代码。
首先拿到 FlutterLoader 对象,那么看下 FlutterLoader 是怎么来的。

FlutterLoader loader = FlutterInjector.instance().flutterLoader();

public final class FlutterInjector {public static void setInstance(@NonNull FlutterInjector injector) {instance = injector;}public static FlutterInjector instance() {accessed = true;if (instance == null) {instance = new Builder().build();}return instance;}public static final class Builder {public Builder setFlutterJNIFactory(@NonNull FlutterJNI.Factory factory) {this.flutterJniFactory = factory;return this;}private void fillDefaults() {if (flutterJniFactory == null) {flutterJniFactory = new FlutterJNI.Factory();}if (executorService == null) {executorService = Executors.newCachedThreadPool(new NamedThreadFactory());}if (flutterLoader == null) {flutterLoader = new FlutterLoader(flutterJniFactory.provideFlutterJNI(), executorService);}}public FlutterInjector build() {fillDefaults();return new FlutterInjector(flutterLoader, deferredComponentManager, flutterJniFactory, executorService);}}
}

通过上面的代码可知,FlutterLoader 时在 FlutterInjector 构造时默认创建。同时值得注意的两点:

  • FlutterInjector 是单例模式,并提供 setInstance() 自行创建。
  • FlutterInjector 通过构造模式构建,并提供自行创建 FlutterJNI.Factory、FlutterLoader 等。

有这两点完全可以 hook FlutterLoader#ensureInitializationComplete()了,但实操下来发现代码量太大,实现难度太高。虽然没法 hook ensureInitializationComplete() 来修改配置,但在实操过程中发现重要信息。
image.png
大致意思是,下面的配置是为上面做兜底。如果我们把 libapp.so 剔除,那么这俩配置都无法生效,那我们可以再加一条来兜底啊,即把下载后 libapp.so 的存储路径配置上去。
结合之前的代码逻辑,shellArgs 最终会在 FlutterJNI#init() 中使用,而 FlutterJNI 又可以在 FlutterInjector 自行创建,那么问题不就简单了:

  • 新建自定义的 FlutterJNI 继承自 FlutterJNI,内部重写 init(),将下载后下载后 libapp.so 的存储路径添加到 shellArgs 中。
  • 在调用 FlutterEngineGroup 构造之前调用 FlutterInjector#setInstance() 将自定义的 FlutterJNI 注入进去。
class CustomFlutterJNI(private val appSOSavePath: String) : FlutterJNI(){override fun init(context: Context,args: Array<out String>,bundlePath: String?,appStoragePath: String,engineCachesPath: String,initTimeMillis: Long) {val hookArgs = args.toMutableList().run {add("--aot-shared-library-name=$appSOSavePath")toTypedArray()}super.init(context, hookArgs, bundlePath, appStoragePath, engineCachesPath, initTimeMillis)}class CustomFactory(private val appSOSavePath: String) : Factory(){override fun provideFlutterJNI(): FlutterJNI {return CustomFlutterJNI(appSOSavePath)}}
}
val appSOSavePath = "******"  // libapp.so 下载保存的存储路径
FlutterInjector.setInstance(FlutterInjector.Builder().setFlutterJNIFactory(CustomFlutterJNI.CustomFactory(appSOSavePath)).build())
val engineGroup = FlutterEngineGroup(context)

小结

通过如下几步实现了 libflutter.so 和 libapp.so 的剔除、上传、动态加载:

  • 自定义 GradleTask 实现在 merged_native_libs/ 中查找指定 so 文件、上传、记录上传信息(写入 assets 中)、剔除。
  • 自定义 GradlePlugin 指定仅在 release 打包中使用,并指定自定义 GradleTask 执行时机。
  • 读取 asstes 信息并下载,下载完成后通过注入 so 加载目录和 hook FlutterJNI 实现动态加载 so 文件,最后调用 FlutterEngineGroup 实现 Flutter 初始化。

实现后的效果非常显著:
image.png

完整代码(仅供参考)

GitHub - StefanShan/flutterSoDynamic: 从 apk 中剔除 libflutter.so 和 libapp.so,并动态下发加载

优化

上面把所有流程跑通了,但有些地方还需要优化:

  • libflutter.so 是根据 flutter 版本生成的,libapp.so 为业务代码生成,所以需要区分上传,即做版本控制,减少重复上传。
  • 同样在下载时,也要根据版本判断,避免重复下载。
  • 动态加载失败时,需要做兜底处理,例如用 H5 页面来替代。

文章来源(更多文章请点击) 青杉

参考资料

到家Flutter动态化瘦身方案的探索 - 墨天轮
Android 重构之旅:动态下发 SO 库
Android 动态链接库 So 的加载
Android编译期动态添加assets





Hi,我是“青杉”,您可以通过如下方式关注我:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/229691.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索Java的魅力

从本篇文章开始&#xff0c;小编准备写一个关于java基础学习的系列文章&#xff0c;文章涉及到java语言中的基础组件、实现原理、使用场景、代码案例。看完下面一系列文章&#xff0c;希望能加深你对java的理解。 本篇文章作为本系列的第一篇文章&#xff0c;主要介绍一些java…

【数据库原理】(6)关系数据库的关系操作集合

基本关系操作 关系数据操作的对象都是关系,其操作结果仍为关系,即集合式操作。关系数据库的操作可以分为两大类&#xff1a;数据查询和数据更新。这些操作都是基于数学理论&#xff0c;特别是集合理论。下面是对这些基本操作的解释和如何用不同的关系数据语言来表达这些操作的…

STM32入门教程-2023版【3-2】推挽输出和开漏输出驱动问题

关注 点赞 不错过精彩内容 大家好&#xff0c;我是硬核王同学&#xff0c;最近在做免费的嵌入式知识分享&#xff0c;帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作! 二、正式点亮一个LED灯 &#xff08;4&#xff09;推挽输出和开漏输出驱动问题 把LED的正负极对换&…

react useEffect 内存泄漏

componentWillUnmount() {this.setState (state, callback) > {return;};// 清除reactionthis.reaction();}useEffect 使用AbortController useEffect(() > { let abortController new AbortController(); // your async action is here return () > { abortCo…

008、所有权

所有权可以说是Rust中最为独特的一个功能了。正是所有权概念和相关工具的引入&#xff0c;Rust才能够在没有垃圾回收机制的前提下保障内存安全。 因此&#xff0c;正确地了解所有权概念及其在Rust中的实现方式&#xff0c;对于所有Rust开发者来讲都是十分重要的。在本文中&…

添加 Android App Links

添加 Android App Links功能 介绍一个简单的效果Android配置Add Url intent filtersAdd logic to handle the intentAssociate website 搭建网页支持AppLinks 介绍 Android App Links 是指将用户直接转到 Android 应用内特定内容的 HTTP 网址。Android App Links 可为您的应用带…

计算机网络--作业

作业一 1、比较电路交换、报文交换和分组报文交换优缺点 电路交换 电路交换是以电路连接为目的的交换方式&#xff0c;通信之前要在通信双方之间建立一条被双方独占的物理通道&#xff08;由通信双方之间的交换设备和链路逐段连接而成&#xff09;。 优点&#xff1a; ①由于…

SpringSecurity-2.7中跨域问题

SpringSecurity-2.7中跨域问题 访问测试 起因 写这篇的起因是会了解到 SSM(CrosOrigin)解决跨域,但是会在加入SpringSecurity配置后,这个跨域解决方案就失效了,而/login这个请求上是无法添加这个注解或者通过配置(WebMvcConfig)去解决跨域,所以只能使用SpringSecurity提供的.c…

MySQL的安装网络配置

目录 一. MySQL5.7的安装 二. MySQL8.0的安装 三. 配置网络访问 思维导图 一. MySQL5.7的安装 1. 解压 2. 将my.ini文件放入到解压文件中 3. 编辑my.ini文件&#xff0c;将路径改为当前路径 4. 进到bin目录下&#xff0c;以管理员身份打开cmd命令窗口 5. 安装MySQL服务 my…

C++Qt6 哈夫曼编码求解 数据结构课程设计 | JorbanS

一、 问题描述 在进行程序设计时&#xff0c;通常给每一个字符标记一个单独的代码来表示一组字符&#xff0c;即编码。在进行二进制编码时&#xff0c;假设所有的代码都等长&#xff0c;那么表示 n 个不同的字符需要 位&#xff0c;称为等长编码。如果每个字符的使用频率相等&…

【hyperledger-fabric】部署和安装

简介 对hyperledger-fabric进行安装&#xff0c;话不多说&#xff0c;直接开干。但是需要申明一点&#xff0c;也就是本文章全程是开着加速器进行的资源操作&#xff0c;所以对于没有开加速器的情况可能会由于网络原因导致下载资源失败。 资料提供 1.官方部署文档在此&#…

车载电子电器架构 —— 电子电气系统开发角色定义

车载电子电器架构 —— 电子电气系统开发角色定义 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 注:本文12000字,深度思考者进!!! 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的…

Redis7.2.3(Windows版本)

1、解压 &#xfeff; &#xfeff; 2、设置密码 &#xff08;1&#xff09; 右击编辑redis.conf文件&#xff1a; &#xfeff; &#xff08;2&#xff09; 设置密码。 &#xfeff; 3、测试密码是否添加成功 &#xfeff; 如上图所示&#xff0c;即为成功。 4、设置…

阿里云 ACK 云上大规模 Kubernetes 集群高可靠性保障实战

作者&#xff1a;贤维 马建波 古九 五花 刘佳旭 引言 2023 年 7 月&#xff0c;阿里云容器服务 ACK 成为首批通过中国信通院“云服务稳定运行能力-容器集群稳定性”评估的产品&#xff0c; 并荣获“先进级”认证。随着 ACK 在生产环境中的采用率越来越高&#xff0c;稳定性保…

第7课 利用FFmpeg将摄像头画面与麦克风数据合成后推送到rtmp服务器

上节课我们已经拿到了摄像头数据和麦克风数据&#xff0c;这节课我们来看一下如何将二者合并起来推送到rtmp服务器。推送音视频合成流到rtmp服务器地址的流程如下&#xff1a; 1.创建输出流 //初始化输出流上下文 avformat_alloc_output_context2(&outFormatCtx, NULL, &…

dmetl5授权查看与更新

1.查看dmetl5授权到期时间 需要登录管理端&#xff0c;菜单栏选择“管理”-“license管理”即可查看授权到期时间。如下图&#xff1a; 2.dmetl5更新授权的方法 dmetl5的<安装目录>\scheduler\config路径下&#xff0c;默认会有一个trail.key的文件&#xff0c;删除后&am…

Java学习苦旅(十六)——List

本篇博客将详细讲解Java中的List。 文章目录 预备知识——初识泛型泛型的引入泛型小结 预备知识——包装类基本数据类型和包装类直接对应关系装包与拆包 ArrayList简介ArrayList使用ArrayList的构造ArrayList常见操作ArrayList遍历 结尾 预备知识——初识泛型 泛型的引入 我…

八大算法排序@选择排序(C语言版本)

目录 选择排序概念算法思想示例步骤1步骤2步骤...n最后一步 代码实现时间复杂度空间复杂度特性总结 选择排序 概念 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法。基本思想是在未排序的序列中找到最小&#xff08;或最大&#xff09;元素&#xf…

Android studio ViewPager2应用设计

一、ViewPager2应用场景&#xff1a; ViewPager2是一个功能强大的滑动容器&#xff0c;提供灵活的页面切换和布局定制功能&#xff0c;使得应用程序界面更加丰富和交互性强&#xff0c;主要应用于以下场景&#xff1a; 1&#xff09;、实现引导页或欢迎页&#xff1a;ViewPag…