货拉拉 Android 动态资源管理系统原理与实践(下)
点击上方蓝字关注我,知识会给你力量
so资源动态化方案
so资源打包问题
在打包so资源的过程中,我们遇到了如下问题。
如何移除apk中的so文件,并将他们收集起来? 如何将多个so文件压缩打包,并生成对应的信息? 如何保证第三方sdk缺少so文件时,不崩溃? so资源打包解决方案
移除并收集apk中的so文件
看到移除 so文件可能有些同学会问,这不是只要在as中删除libs目录就搞定了么?这样会有几个问题
对于多个module的工程,我们需要逐个删除每个module下的libs目录,麻烦而且容易出错。 对于三方aar包中的so文件,我们就没法删除了。 so文件变化需要人工维护,容易出错。
出于以上考虑,我们认为,在编译时期,自动删除并收集so文件是最优解,那么在编译时期进行以上操作呢?我们注意到as在进行build时,会有大量的系统提供的task在运行,那么这些系统task是否就完成了编译并收集各个地方的so文件,并把他们打包进apk的任务呢?
看一眼这幅超级复杂的apk构建流程图,嗯,可以看到,系统确实会在apkBuilder构建前,将本地的c/c++文件编译成so库,并将第三方的so库一起打包到apk中,我们需要寻找的就是收集所有so库的系统Task
通过查找资料,我们发现,确实有2个系统task会用来处理合并so库并且删除debug符号(注意,task名称可能与此处不完全相同)。
Task名称 | 实现类 | 作用 | 结果保存目录 |
---|---|---|---|
mergeDebugNativeLibs | MergeNativeLibsTask | 合并所有依赖的 native 库 | intermediates/merged_native_libs |
stripDebugDebugSymbols | StripDebugSymbolsTask | 从 Native 库中移除 Debug 符号 | intermediates/stripped_native_libs |
一般来说,应该在stripSymbols结束后去剔除 stripped_native_libs 目录下的文件。 但是剔除debug符号操作,可能导致不同as版本得到的so文件md5码不相同。 因此,我们采用了可配置方案,可以由用户配置决定,在MergeNativeLibsTask或者stripDebugDebugSymbols后,执行删除输出文件夹中so文件操作。 第三方 so 一般都是 Release 编译出来的,不进行strip影响也不大。而我们自己的so文件,则strip操作可能会对so体积造成较大影响。 下面我们以在MergeNativeLibsTask之后,执行删除输出文件夹中so文件的方式,进行讲解。
由于我们有多个gradle task需要执行,因此我们创建了一个名为dynamic_plugin的android plugin工程,内部包含了多个gradle task。关于as中新建插件的方法,请自行搜索其他博客,本文因为篇幅问题,不进行讲解。
在我们的dynamic_plugin插件内部,我们新建一个名为DeleteAndCopySo的gradle task并将它插入到系统的merge和strip之间,利用该Task完成删除merged_native_libs目录下对应so文件,并将其拷贝到我们指定的新目录下。这样apk打包时,就不会包含动态化的so文件了
//获取系统的mergeTask
Task mergeNativeTask = TaskUtil.getMergeNativeTask(project);
//获取系统的skipTask
Task stripTask = TaskUtil.getStripSymbol(project);
//创建我们的DeleteAndCopySo task
Task deleteTask = project.getTasks().create(PluginConst.Task.DELETE_SO);
deleteTask.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
deleteAndCopySo(project, param);
}
});
//将我们的Task插入到merge和strip之间
stripTask.dependsOn(deleteTask);
deleteTask.dependsOn(mergeNativeTask);
如何将多个so文件压缩打包,并生成对应的信息?
上一步中,我们已经将so文件从系统apk构建流程中删除,并且拷贝到了指定目录下。那么现在我们应该做什么呢?
将so文件打包成.zip压缩包。 生成该资源对应的实体类DynamicPkgInfo。包括文件id,文件名称,文件类型,版本号,下载地址等基本信息,以及文件md5,文件长度等校验信息。以及压缩包下的所有子文件及文件夹相关信息。 将该zip文件上传到服务器,以方便下载和使用。
对于上述这些步骤,在我们的货拉拉动态管理系统初始版本中,我们采用了自己打zip包,自己写java代码来生成资源信息的方式。
但是在后来的使用过程中,我们发现,手动进行这些步骤,很繁琐且容易出错,我们需要有一种自动化的方式进行上述过程。
我们在dynamic_plugin插件内部,再新增一个ZipSoTask来进行压缩so文件夹,以及生成资源信息常量的操作。该task在DeleteAndCopySo之后,stripe系统task之前执行。
//执行将so文件夹压缩成.zip操作
List<DynamicUtil.ZipInfo> zips = DynamicUtil.zipFolder(new File(param.getmInputSo()),outDir);
//根据so文件和zip压缩包信息,生成md5,length等校验信息并存储
DynamicUtil.createPkgDatas(mPkgList,zips,PluginConst.Type.SO);
//根据资源信息类生产java文件
param.getmFileCreate().createFile(mPkgList,param);
前2步,压缩so文件,和根据so文件,zip文件生成校验信息并存储比较简单,就不详细说了。 第3步,根据前面的信息,直接生产java文件,我们使用了第三方的开源库javapoet。 JavaPoet 是 Square 公司推出的开源 Java代码生成框架,提供接口生成 Java 源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。 大致的生产代码如下,首先生成一个DynamicResConst类,之后遍历zip压缩资源列表,为列表中的每一个资源,生成一个static final的常量,表示每个资源,最后生成java文件。
//创建DynamicResConst类,用来存储资源实体常量
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( "DynamicResConst" )
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
//遍历资源列表,生成对应实体类DynamicPkgInfo
for (DynamicPkgInfo pkg : pkgs) {
FieldSpec fsc = createField(pkg);
typeBuilder.addField(fsc);
}
//插件java文件,并写入
JavaFile javaFile = JavaFile.builder(param.getmCreateJavaPkgName(), typeBuilder.build()).build();
try {
javaFile.writeTo(new File(param.getmOutputPath()));
} catch (Exception e) {
}
至于最后一步,将so压缩包上传到服务器,我们在配置文件中提供了一个上传方法,不过默认实现为空,用户可以手动上传也可以修改默认方法实现自动上传。自动生成的资源文件中,版本号需要手动修改控制,下载地址手动上传的话,也需要手动修改。
保证第三方sdk在缺少so文件时,不崩溃
很多三方sdk都要求在应用启动时,进行初始化,一个使用so库的类的典型类代码如下:
public class ThirdLib{
//静态方法加载so库
static{
System.loadLibrary("third");
}
//native方法示例
public native void testNative();
//java方法示例
public void test();
//......其他内容省略
}
如果此时so库没有被加载好,直接使用ThirdLib类,则会执行static代码段中的System.loadLibrary方法,导致UnsatisfiedLinkError的错误,造成App崩溃。由于我们无法直接修改第三方sdk的源码,因此我们只能采用动态字节码技术,替换掉System.loadLibrary方法了。 我们采用android的transform加asm技术,动态的将System.loadLibrary替换成我们自己的SoLoadUtil中的loadLibrary方法。 Gradle Transform 是 Android 官方提供给开发者在项目构建阶段,即由 .class 到 .dex 转换期间修改 .class 文件的一套 API, 无论是class还是jar都可以控制。 ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。
具体执行替换的代码如下,在Asm框架中的MethodVisitor类中,重写visitMethodInsn方法,判断该方法的拥有者,名称和参数列表和System.loadLibrary对应,则我们将他替换为我们的SoLoadUtil.loadLibrary方法
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if(TextUtil.equals(owner, PluginConst.SYSTEM_CLASS) &&
TextUtil.equals(name, PluginConst.LOAD_LIBRARY_METHOD) &&
TextUtil.equals(descriptor, PluginConst.LOAD_LIBRARY_DESC)){
owner = "com/xxx/xxx/dynamicres/util/SoLoadUtil" ;
mv.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, false);
return;
}
mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
替换后的方法主要逻辑为,使用第三方库Relinker替代System.loadLibrary方法进行so文件加载,并且catch住加载异常,来防止应用直接奔溃,并且在加载so库异常时,将该库的名称保存下来,在我们的so包被正常下发加载后,再次调用本方法,将so库load到系统中。
protected void realSoLoad(Context c, String libName) {
try {
ReLinker. recursively ().loadLibrary(c, libName);
removeFormWaitList(libName);
} catch (Throwable t) {
addToWaitList(libName);
}
}
对于so库没有加载完成,直接使用ThirdLib类导致System.loadLibrary方法被调用,导致的应用崩溃问题,我们已经解决了。 而对于直接调用ThirdLib类的testNative方法,导致的应用崩溃问题,则无法解决。因此需要看情况决定是否能够接受该种崩溃,以及是否将引发该问题的so库进行动态化。
我们只需要在工程的主Application中,直接调用loadSo方法,对so动态资源进行加载。加载完成后,so库就能正常使用了。
public void loadSo(DynamicSoInfo soInfo, ILoadSoListener listener) {
if (soInfo == null) {
return;
}
//根据本机abi,获取适合的动态资源实体类DynamicPkgInfo
DynamicPkgInfo pkg = soInfo.getPkgInfo(Build.SUPPORTED_ABIS);
if (pkg == null) {
return;
}
//如果该so资源,已经被加载缓存过了,直接listener的成功回调,并返回
if (isLoadAndDispatchSo(pkg, listener)) {
return;
}
//开启资源加载,和普通资源流程一致
DynamicResManager manager = DynamicResManager.getInstance();
manager.load(pkg, new DefaultLoadResListener() {
@Override
public void onSucceed(LoadResInfo info) {
super.onSucceed(info);
//so成功下载校验后,执行加载逻辑
handleLoadSoSucceed(pkg, info, listener);
}
});
}
so资源加载和应用问题
在so资源的加载和应用过程中,我们发现了如下问题
如何判断系统需要哪些so文件,并按需正确加载? 如何下载so文件,并保证它的正确性? 如何将下载的动态so文件,正确应用到系统中? so资源加载和应用解决方案
如何判断系统需要哪些so文件,并正确下载安装?
我们把arm64-v8a,armeabi-v7a等abi分开打包,上传到服务器。使用时,本地判断abi支持,下载对应的abi包。这样做的优点是节省流量和下载后占据的空间。
至于判断系统需要哪些abi的so包,并按需正确应用,则比较简单,读取系统的SUPPORTED_ABIS常量,这里包含了系统支持的abi列表,而排在前面的表示优先级更高。我们只要遍历它,然后查找我们的动态资源包是否有匹配,就达到了正确加载的目标。
private Map<String,DynamicPkgInfo> mSoInfos;
public DynamicPkgInfo getPkgInfo(){
//获取本地系统支持的abi列表
String[] supportAbis = Build.SUPPORTED_ABIS;
if(supportAbis==null || supportAbis.length== 0 ){
return null;
}
//遍历abi支持列表
for(String abi : supportAbis){
//从so动态资源中,查找对应的abi信息
DynamicPkgInfo pkg = mSoInfos.get(abi);
//找到则直接返回该信息
if(pkg != null){
return pkg;
}
}
return null;
}
如何下载so文件,并保证它的正确性?
复用通过资源加载流程即可。
如何将下载的动态so文件,正确应用到系统中?
这里需要首先了解一下,系统加载so库的工作流程,当我们调用 System#loadLibrary("xxx" ) 后,Android Framework 都干了些了啥?Android 的 so 加载机制,大致可以分为以下四个环节。
安装 APK 包的时候,PMS 根据当前设备的 abi 信息,从 APK 包里拷贝相应的 so 文件。 启动 APP 的时候, Android Framework 创建应用的 ClassLoader 实例,并将当前应用相关的所有 so 文件所在目录注入到当前 ClassLoader 相关字段。 调用 System.loadLibrary("xxx"), framework 从当前上下文 ClassLoader 实例(或者用户指定)的目录数组里查找并加载名为 libxxx.so 的文件。 调用 so 相关 JNI 方法。
而我们这里,由于so文件不存在于apk当中,而是需要动态下载,所以我们显然不能直接使用系统的System.loadLibrary方法加载so文件。
而动态加载so的方法,在热修复和插件化框架中,已经比较成熟了,我们参考了市面上的开源框架后,选择了腾讯的Tinker框架的加载方案,即使用反射classloader 将 so 包的路径写入 nativeLibraryPathElements 数组最前面,其流程图和解释如下图所示 。注意,此方法不同的android版本将有不同的实现。下面示例代码基于android9.0版本。
private static void install(ClassLoader classLoader, File soFolder) throws Throwable {
Field pathListField = findField(classLoader, "pathList" );
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = findField(dexPathList, "nativeLibraryDirectories" );
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
libDirs.add(0, soFolder);
Field systemNativeLibraryDirectories =
findField(dexPathList, "systemNativeLibraryDirectories" );
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
Method makePathElements =
findMethod(dexPathList, "makePathElements" , List.class);
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.
invoke(dexPathList, libDirs);
Field nativeLibraryPathElements = findField(dexPathList, "nativeLibraryPathElements" );
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
pathList变量:DexPathList类的实例。 nativeLibraryDirectories列表:包含了本App自带so文件的查找路径(如data/app/包名/lib/arm64) systemNativeLibraryDirectories列表:包含系统so文件查找路径(如system/lib64) makePathElements:系统使用此方法,为所有so文件,生成对应的 NativeLibraryElement对象 nativeLibraryPathElements数组:系统用来存储所有的so文件路径
当外界调用System.loadLibrary方法时,系统最终会调用到DexPathList类的findLibrary方法,该方法会在nativeLibraryPathElements数组中查找对应的路径,我们将自己的so加入到nativeLibraryPathElements最前面,由此达到动态加入so的目标。
so资源动态化的tips
为何要使用Relinker加载So文件
假如我们有2个so文件,libA.so 和 libB.so,libA依赖libB,则当我们调用System.loadLibrary("libA") 的时候,android framework 会通过上面提到的调用链最终通过 dlopen 加载 libA.so 文件,并接着通过其依赖信息,自动使用 dlopen 加载 libB.so。 在 Android N 以前,只要将 libA.so 和 libB.so 所在的文件目录路径都注入到当前 ClassLoader 的 nativeLibraryPathElements 里,则在加载 so 插件的时候,这两个文件都能正常被找到。 从 N 开始,libA.so 能正常加载,而 libB.so 会出现加载失败错误。 因为Android Native 用来链接 so 库的 Linker.cpp dlopen 函数 的具体实现变化比较大(主要是引入了 Namespace 机制):以往的实现里,Linker 会在 ClassLoder 实例的 nativeLibraryPathElements 里的所有路径查找相应的 so 文件。 更新之后,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libA.so 文件能找到,而 libB.so 找不到的情况。 至于 Namespace 机制的工作原理了,可以简单认为是一个以 ClassLoader 实例 HashCode 为 Key 的 Map,Native 层通过 ClassLoader 实例获取 Map 里存放的 Value(也就是 so 文件路径集合)。
解决该问题有如下几种思路:
自定义 System.loadLibrary,加载 SO 前,先解析 SO 的依赖信息,再递归加载其依赖的 SO 文件,这是开源库soLoader的解决方案。 自定义 Linker,完全自己控制 SO 文件的检索逻辑 ,这是开源库Relinder的解决方案。 替换 ClassLoader 。
本着不重复造轮子的原则,项目中使用了Relinker开源库,用来加载so文件。
so库依赖分析工具
想要把 so 动态化技术应用到 APK 的瘦身项目中来,除了分析哪些 so 文件体积占比比较大之外,最好的做法是将其依赖的所有 so 文件一定挪到插件包里。怎么了解 APK 里所有 so 文件具体的依赖信息呢?这里推荐一款 Google 开源的 APK 解析工具android-classyshark,除了提供分析 APK dex/so 依赖信息之外,它还提供了 GUI 可视化界面,非常适合快速上手。
so动态化流程
so资源应用流程
获取系统支持abi列表,根据该列表,找到合适的so动态资源实体类。 如果该资源已经被加载缓存,则直接回调加载成功。 否则,开始资源通用加载流程,并异步等待资源加载成功(流程见第5章)。 再次判断下载校验后的资源,是否支持本机abi。 将so包路径加入DexPathList的数组头部。 遍历等待加载so列表,尝试加载所有so文件,并将成功加载的so文件,移除该列表。 将资源id和本地路径加入缓存,防止so被重复加载。 回调加载完成监听器。
SoLoadUtil.loadLibrary方法流程
从上一章我们知道,我们会使用transform api加asm框架,将系统的System.loadLibrary方法替换成我们的SoloadUtil.loadLibrary方法。我们替换系统方法的目的。一个是为了保证so库不存在时,程序不崩溃,另外一个就是so库下载校验完成后,能自动完成之前失败的加载,为此,我们设计了如下流程。
其他方法调用到我们的SoloadUtil时,我们判断我们的加载系统是否初始化完成 已完成,则调用Relinkder库尝试加载so文件,未完成则将该so库加入待加载队列中。 如果Relinker加载so文件成功,我们从待加载队列中移除so,并且完成本次加载。 否则我们依然将so文件加入待加载队列中。 根据上面的so加载流程,当so动态资源真正下载校验完成后,我们会遍历待加载队列,并完成所有之前未成功的so库加载。
dynamic_plugin插件流程
整体流程
前面我们已经分析了通用资源加载,内置资源应用,完成了动态资源管理系统的主要部分。只剩下资源打包部分了,而所有资源的打包操作,都由dyanmic_plugin插件来完成。为了完成打包功能,我们决定在这个dynamic_plugin插件内部,新建3个Task。
Hook System.loadLibrary方法的TransformTask。 系统打包流程中,删除并拷贝so文件的DeleteAndCopySoTask。 压缩so资源和其他多个文件资源(例如帧动画)的ZipResTask。 为每个动态资源生成其对应的DynamicPkgInfo常量的功能,仅实现为一个普通方法。
所以主流程也就出来了
读取并解析dynamic_plugin.gradle配置文件。 根据配置信息,决定是否将3个task加入任务队列。 启动任务队列。
TransformTask流程
该task流程,主要就是通过tranform api和asm框架的使用,我们在其中加入了扫描class范围的可配置项。
等待asm框架扫描class。 判断该class名称是否在我们配置的替换列表中,如果不在,就直接返回。 创建ClassVisitor和MethodVisitor,等待asm框架扫描每个方法。 如果该方法的名称,参数列表和调用者,都和System.loadLibrary方法相符合。 我们替换为自己的SoloadUtil.loadLibrary方法。
DeleteAndCopySoTask流程
根据配置文件,找到系统的merge和strip task。 将我们的task插入到2个系统task之间,并等待系统回调我们的doLast方法。 遍历系统的mergeTask的输出目录,判断该so文件是否在我们配置的待扫描列表中。 如果配置了需要拷贝so文件,则我们将它拷贝到指定位置。 如果配置了需要删除so文件,则我们将该so文件删除。
ZipResTask流程
拷贝字体文件,将文件信息加入资源列表。 压缩帧动画文件,将压缩后的文件信息加入资源列表。 压缩so文件,将压缩后的文件信息加入资源列表。 压缩zip文件夹下文件,将压缩后的文件信息加入资源列表。 遍历资源文件,为其生成相应的资源实体类DynamicPkgInfo。
dynamic_plugin插件类设计
可以与第4章,整体架构图结合起来看。
系统插件层
DynamicPlugin类,实现了系统gradle插件的plugin接口,为我们整个插件的入口,主要解析配置文件,并按照配置文件创建task信息。
DynamicParam类,提供了存储并解析dyanmic_plugin配置文件的方法。
任务模块层
ITask接口,代表了一个我们定义的任务。
DeleteAndCopySoTask,删除并拷贝so文件任务。
TransfomrTask,替换系统System.loadLibrary方法任务。
ZipResTask,压缩so和其他文件,并生成对应的java资源实体类方法。
底层实现层
SystemLoadClassVisitor类,Asm框架的class访问类。
SystemLoadMethodVisitor类,Asm框架的method访问类,用于替换System.loadLibrary方法。
JavaFileCreate类,使用javapoet框架产生java文件。
其他辅助类,在此省略
类uml图
dynamic_config.gradle配置文件
该配置文件主要包含了配置dynamic_plugin插件运行步骤,插件输入输出路径,so文件扫描路径等信息。
dynamic_config = [
//是否执行替换System.loadlibrary操作
is_replace_load_library: false,
//是否执行替换System.load操作
is_replace_load : false,
//是否执行删除so文件操作
is_delete_so : false,
//是否执行将so文件拷贝到其他目录操作
is_copy_so : false,
//是否执行将动态资源打包,并生成java文件操作
is_zip_res : false,
//是否执行将so文件打包,并生成java文件操作
is_zip_so : false,
//是否自动上传所有资源,上传方法为dynamic_upload
is_upload_res : false,
//插件是否工作在Release模式下
is_release_type : isReleaseBuildType(),
//是否打印debug日志
is_debug_log : true,
//自动创建java文件时的包名
create_java_pkg_name : 'com.test' ,
]
/**
* 配置要删除和拷贝的so文件
* map的key为压缩包名称,值为压缩包包含的so文件列表
* key为debug_all_test时,会压缩所有so包
*/
dynamic_scan_so_map = [
guang_dong : [ 'libpajf.so' , 'libpajf_av.so' , 'libsqlite.so' ],
]
dynamic_so_config = [
//so文件忽略列表,该表中的文件,不会被扫描。不在该列表中的文件都会被扫描
// (dynamic_scan_so_map为空时,本列表才生效)
ignore_so_files: [],
//so文件扫描abi目录,不在该目录下的so将不被扫描
scan_so_abis : [ "arm64-v8a" , "armeabi-v7a" ],
//拷贝出来的so文件夹前缀,ignore_so_files生效时使用
so_input_prefix: 'test' ,
]
dynamic_lib_list = [
//只有该列表中的包名,才会执行替换System.loadlibrary操作
//输入debug_all_test,则会替换所有System.loadLibrary方法,用于测试
scan_load_library_pkgs : [],
//在该列表中的包名或者类名,不会执行替换System.loadlibrary操作,和上面的配置可以同时生效
ignore_load_library_pkgs: [],
]
//该配置不要改动内容,需要改变路径的,直接改变对应的方法内容即可
dynamic_dir = [
//产生文件的输出目录
output : createOrGetOutputPath(),
//字体资源输入目录
typeface_input : createOrGetInputTypafacePath(),
//帧动画资源输入目录
frame_anim_input: createOrGetInputFrameAnimPath(),
//so文件资源输入目录
so_input : createOrGetInputSoPath(),
//zip包输入目录
zip_input : createOrGetInputZipPath()
]
//该配置项,配置了android 2个gradle task的名称
//主工程的mergeNativeLibs合并所有依赖的 native 库
//主工程的stripDebugSymbols从 Native 库中移除 Debug 符号。
dynamic_task = [
//自定义的task运行哪里
//true为mergeNativeLibs之后,stripDebugSymbols之前
//false为stripDebugSymbols之后,package之前
isTaskRunAfterMerge : true,
//debug状态下,mergeNativeLibs的task名称
debugMergeNativeLibs : "mergeDebugNativeLibs" ,
//release状态下,mergeNativeLibs的task名称
releaseMergeNativeLibs : "mergeReleaseNativeLibs" ,
//debug状态下,stripDebugSymbols的task名称
debugStripDebugSymbols : "stripDebugDebugSymbols" ,
//release状态下,stripDebugSymbols的task名称
releaseStripDebugSymbols: "stripReleaseDebugSymbols" ,
//debug状态下,系统打包task名称
debugPackage : "packageDebug" ,
//release状态下,系统打包task名称
releasePackage : "packageRelease" ,
//debug状态下,mergeNativeLibs的输出目录
debugNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/debug/out/lib" ,
//release状态下,mergeNativeLibs的输出目录
releaseNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/release/out/lib" ,
//debug状态下,,stripDebugSymbols的输出目录
debugStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/debug/out/lib" ,
//release状态下,,stripDebugSymbols的输出目录
releaseStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/release/out/lib" ,
]
//该闭包可以自动将文件上传到服务器,参数列表为资源id,资源文件路径
//我们可以再次执行上传服务器操作,并返回对应的url。
//当然也可以不实现上传操作,并自己手动上传资源。
dynamic_upload = {
id, path ->
println( "dynamic_upload id $ { id } ,path $ { path }" )
return 'http://url'
}
优化效果
通过引入动态资源管理系统,并将一键报警sdk相关的so文件和其他普通资源动态化后,货拉拉用户端的包体积减少了8M,从54M变为了46M。后继将会继续尝试进行其他so文件的动态化。
参考文献
https://www.jianshu.com/p/260137fdf7c5
https://mp.weixin.qq.com/s/X58fK02imnNkvUMFt23OAg
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
更文不易,点个“三连”支持一下👇