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.hqwc.cn/news/315938.html

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

相关文章

nccl 源码安装与应用示例 附源码

1&#xff0c; 官方下载网址 注意&#xff0c;本文并不使用nv预编译的包来安装&#xff0c;仅供参考&#xff1a; NVIDIA Collective Communications Library (NCCL) | NVIDIA Developer 2&#xff0c;github网址 这里是nv开源的nccl源代码&#xff0c;功能完整&#xff0c;不…

侯捷C++ 2.0 新特性

关键字 nullptr and std::nullptr_t auto 一致性初始化&#xff1a;Uniform Initialization 11之前&#xff0c;初始化方法包括&#xff1a;小括号、大括号、赋值号&#xff0c;这让人困惑。基于这个原因&#xff0c;给他来个统一&#xff0c;即&#xff0c;任何初始化都能够…

C#编程-使用条件构造

使用条件构造 作判定是人的基本能力。判定也是可收编进程序。这有助于确定程序执行指令的顺序。 您可用条件构造来控制程序的流程。条件构造允许您基于被求职的表达式的结果来执行选定语句。 可以包含在C#程序中的各种条件构造是: if…else 构造switch…case 构造if…else构…

git 如何撤销历史某次merge

git&#xff0c;如何 撤销某一次历史提交或merge&#xff0c;并保留该版本的后续提交&#xff1f; 场景1&#xff1a; 你有两个功能迭代版本的分支&#xff0c;一个是 15 号上线&#xff0c;一个是25号上线。5号的时候产品突然说&#xff0c;这两个版本一起上&#xff0c;然后…

【LabVIEW FPGA入门】创建第一个LabVIEW FPGA程序

本教程仅以compactRIO&#xff08;FPGA-RT&#xff09;举例 1.系统配置 1.1软件安装 FPGA-RT 1. LabVIEW Development System (Full or Professional) 2. LabVIEW Real-Time Module 3. LabVIEW FPGA Module 4. NI-RIO drivers 1.2硬件配置 1.使用线缆连接CompactRIO至主机…

软件测试/测试开发丨Pytest结合数据驱动

安装yaml pip install pyyaml pytest结合数据驱动yaml 工程目录结构 数据准备 读取excel文件 openpyxl库的安装 openpyxl库的操作 pytest结合csv实现数据驱动 csv文件介绍 pytest结合json实现数据驱动 最后感谢每一个认真阅读我文章的人&#xff0c;礼尚往来总是要有的&…

Jmeter实现分布式并发

Jmeter实现分布式并发&#xff0c;即使用远程机执行用例。 环境&#xff1a; VMware Fusion Windows系统是win7。 操作过程 1、Master在jmeter.properties添加remote_hosts 2、Slave在jmeter.properties添加server_port 同时把remote_hosts修改为和主机&#xff08;Master…

matlab生成列是0-255渐变的图像

图像大小&#xff1a;640512 8位灰度图 %% 生成图像 %大小&#xff1a;640*512 %类型&#xff1a;灰度图 %灰度值&#xff1a;列按照0-255渐变&#xff0c;故命名为column shade。 clc,clear all,close all; %输入的图 imadouble(imread(lenna2.bmp));%原图 imargb2gray(ima)…

ubuntu系统没有网络图标的解决办法

参考文章:https://blog.csdn.net/qq_56922632/article/details/132309643 1. 执行关闭网络服务的命令&#xff0c;关闭网络服务sudo service NetworkManager stop2. 删除网络的状态文件sudo rm /var/lib/NetworkManager/NetworkManager.state3. 修改网络的配置文件sudo vi /etc…

SpringBoot—支付—支付宝

一、流程 二、沙箱操作 1.用支付宝账号登录【开放控制平台】创建应用获取 appid 2.选择沙箱模拟环境 3.沙箱应用-》获取appid(一个appid绑定一个收款支付宝账户) 4.利用开发助手工具生成RSA2密钥 公钥&#xff1a;传给支付宝平台 私钥&#xff1a;配置代码中&#xff0c;…

基于simiulink的flyback反激型电路建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 Flyback反激型电路的基本原理 4.2 Flyback反激型电路的数学建模 4.3 Flyback反激型电路的仿真方法 5.完整工程文件 1.课题概述 flyback反激型电路建模与仿真。反激变换器在开关管导通时电源将电能…

opencv期末练习题(3)附带解析

创建黑色画板&#xff0c;并支持两种画图功能 import mathimport cv2 import numpy as np """ 1. 创建一个黑色画板 2. 输入q退出 3. 输入m切换画图模式两种模式&#xff0c;画矩形和画圆形。用户按住鼠标左键到一个位置然后释放就可以画出对应的图像 "&qu…