背景
最近在做包体积优化,在完成代码混淆、压缩,裁剪ndk支持架构,以及资源压缩(如图片转webp、mp3压缩等)后发现安装包的中占比较大的仍是 so 动态库依赖。
具体查看发现 libflutter.so 和 libapp.so 的体积是最大的,这两个动态库都是 flutter 集成进来的。
结合项目中 Flutter 的应用,Flutter 页面都是作为二级页面使用,而且页面使用频率很低,所以是不是可以把这两个 so 从 apk 中剔除,在应用启动后再动态下发呢?
如果可以实现,那么包体积又可以缩减 13.8 M,包体积在原基础上立减一半,收益非常可观!开搞!
实战
libflutter.so & libapp.so 如何引入项目的?
项目是以远程依赖方式引入 flutter,即 flutter 开发完成后打包 aar 发布到公司 maven。通过解压已打包的 aar 发现,aar 中仅有 libapp.so,并没有 libflutter.so。而唯一提到 libflutter.so 的只有打包时生成的 pom 文件。
那么就从宿主项目入手。要远程依赖 flutter,需要指定 repositories{} 。通过配置发现,除了公司 maven 仓库地址,还需要额外配置一个 "storage.flutter-io.cn/download.fl... pom 文件,可以猜测 libflutter.so 是在依赖解析过程中引入到项目中的。
groovy
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
- 明确只在打 release 包时才需要剔除(因为谁关心 debug 包包体积呀!)
- 确定剔除 Task 执行的时机。剔除要在 merge 所有 so 之后才行,通过查看 task 列表,发现 "mergeReleaseNativeLibs" 就是非常不错的时机。
java
public class FlutterDynamicPlugin implements Plugin<Project> {
@Override
public 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 Task
EngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);
//指定自定义 Task 执行时机:mergeReleaseNativeLibs -> flutterSoDynamicRelease
Task mergeSOTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");
mergeSOTask.finalizedBy(engineSoDynamicTask);
});
});
}
}
}
自定义 Gradle Task
- 找到 libflutter.so
- 上传
- 剔除
- 记录上传信息(用于运行时下载)
java
public class EngineSoDynamicTask extends DefaultTask {
@Input
public String mergeNativeLibsOutputPath;
@TaskAction
public void optimizeEngineSo() {
//从 app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a 中找到 libflutter.so
File 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 三者先后顺序
java
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),所以先看它的构造函数中有何逻辑。
java
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 的。
java
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();
java
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() 来修改配置,但在实操过程中发现重要信息。
大致意思是,下面的配置是为上面做兜底。如果我们把 libapp.so 剔除,那么这俩配置都无法生效,那我们可以再加一条来兜底啊,即把下载后 libapp.so 的存储路径配置上去。
结合之前的代码逻辑,shellArgs 最终会在 FlutterJNI#init() 中使用,而 FlutterJNI 又可以在 FlutterInjector 自行创建,那么问题不就简单了:
- 新建自定义的 FlutterJNI 继承自 FlutterJNI,内部重写 init(),将下载后下载后 libapp.so 的存储路径添加到 shellArgs 中。
- 在调用 FlutterEngineGroup 构造之前调用 FlutterInjector#setInstance() 将自定义的 FlutterJNI 注入进去。
kotlin
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)
}
}
}
kotlin
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 初始化。
实现后的效果非常显著:
完整代码(仅供参考)
GitHub - StefanShan/flutterSoDynamic: 从 apk 中剔除 libflutter.so 和 libapp.so,并动态下发加载
优化
上面把所有流程跑通了,但有些地方还需要优化:
- libflutter.so 是根据 flutter 版本生成的,libapp.so 为业务代码生成,所以需要区分上传,即做版本控制,减少重复上传。
- 同样在下载时,也要根据版本判断,避免重复下载。
- 动态加载失败时,需要做兜底处理,例如用 H5 页面来替代。
文章来源(更多文章请点击) 青杉
参考资料
到家Flutter动态化瘦身方案的探索 - 墨天轮
Android 重构之旅:动态下发 SO 库
Android 动态链接库 So 的加载
Android编译期动态添加assets