佛山企业门户网站建设,吉林建设工程信息网站,做吗查网站的流量,装饰公司管理系统背景
最近在做包体积优化#xff0c;在完成代码混淆、压缩#xff0c;裁剪ndk支持架构#xff0c;以及资源压缩#xff08;如图片转webp、mp3压缩等#xff09;后发现安装包的中占比较大的仍是 so 动态库依赖。 具体查看发现 libflutter.so 和 libapp.so 的体积是最大的在完成代码混淆、压缩裁剪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 仓库地址还需要额外配置一个 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
明确只在打 release 包时才需要剔除因为谁关心 debug 包包体积呀确定剔除 Task 执行的时机。剔除要在 merge 所有 so 之后才行通过查看 task 列表发现 “mergeReleaseNativeLibs” 就是非常不错的时机。
public class FlutterDynamicPlugin implements PluginProject {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
找到 libflutter.so上传剔除记录上传信息用于运行时下载 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({//...ListString 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() 来修改配置但在实操过程中发现重要信息。大致意思是下面的配置是为上面做兜底。如果我们把 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: Arrayout 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 初始化。
实现后的效果非常显著
完整代码仅供参考
GitHub - StefanShan/flutterSoDynamic: 从 apk 中剔除 libflutter.so 和 libapp.so并动态下发加载
优化
上面把所有流程跑通了但有些地方还需要优化
libflutter.so 是根据 flutter 版本生成的libapp.so 为业务代码生成所以需要区分上传即做版本控制减少重复上传。同样在下载时也要根据版本判断避免重复下载。动态加载失败时需要做兜底处理例如用 H5 页面来替代。 文章来源(更多文章请点击) 青杉 参考资料
到家Flutter动态化瘦身方案的探索 - 墨天轮Android 重构之旅动态下发 SO 库Android 动态链接库 So 的加载Android编译期动态添加assets Hi我是“青杉”您可以通过如下方式关注我