动态资源办理体系是货拉拉现在运用的用于办理离线so、动画资源、字体文件的组件,对于减小包体积起着重要作用。详细运用办法参阅开源代码中介绍(github.com/HuolalaTech… )
-
前语
- 随着公司业务的扩展,货拉拉用户端apk包的体积也不断变大,曩昔一年,用户端android组进行了很多的减肥作业,取得了较为明显的效果。再运用常规办法,,现已很难优化包体积了。
- 咱们能够把一些运用频率相对较低的资源不打包进apk,并在需求时下载到本地(例如动画文件,字体,zip紧缩包,so库等)
- 咱们留意到,货拉拉用户端apk中,运用了35个以上的so库,而且都支撑arm64-v8a和armeabi-v7a这2种abi,成果便是so体积成倍上涨。用户端出产环境下的apk,解紧缩后,寄存so包的lib目录,占据了整个运用41%的大小。
- 因而动态资源办理体系是下一个优化的要点,动画,字体和zip包仅仅一般文件,彻底能够支撑动态下载并运用。而so文件本质上便是一种可动态加载并履行的文件,将 so文件动态下发是切实可行的,可是要将它从 apk中除掉并确保稳定性并不是一件易事。
-
行业计划
- 未找到现成的github项目或许三方sdk计划,来完结动态资源办理。
- 部分博客供给了动态办理so文件的思路,可是短少完整流程。
- 行业现在并未供给完整的成熟计划供咱们运用,需求咱们自己造轮子。
-
功用和计划
-
完结功用
-
资源分类,预界说了字体,帧动画,so这3种内置资源,以及单个文件,多个文件这2种可自界说资源。
-
供给通用的加载动态资源办法,一切资源均可由此加载。
-
内置资源,供给默许的运用办法,外部能够直接运用。自界说资源,用户自行决议怎么运用。
-
对于一切资源,供给可装备的方便快捷打包办法,减少手动操作。
-
几个概念
-
资源加载:将动态资源经过下载,校验,解压等办法,映射到本地文件的进程。
该进程对一切资源通用,sdk运用方无需修正资源加载办法。
-
资源运用:动态资源对应的本地文件运用到详细业务中。例如动态字体资源的运用,便是为TextView设置一个新的字体。
该进程每个资源不同,sdk运用方无需修正内置资源的运用办法,对于自界说资源,需求运用方自行决议运用办法。
-
资源打包:包括生成一个待上传的资源文件,以及生成资源的Java描绘(DynamicPkgInfo类)。so资源还包括了一些办法的hook操作。
该进程对一切资源都适用,共同运用可装备的dyanmic_res_resplugin插件完结。sdk运用方无需修正资源打包办法,可是可经过装备dyanmic_plugin.gradle文件,装备打包进程。
-
通用资源加载
-
怎么确定资源现已下载过了,避免重复下载?
Java代码中,运用DynamicPkgInfo类来描绘资源,该类中包括了资源的版别号。咱们比较该类和本地数据库中的资源版别号,假设不同,才会下载资源。
-
下载资源是否供给多线程下载,断点续传等功用?
本sdk只供给了下载接口,未供给实际下载功用,因而如需这些功用,需求调用者自己完结。
-
怎么校验资源,避免被篡改?
DynamicPkgInfo类中包括了资源校验信息,咱们利用该类,对下载好的文件进行md5码,文件长度,文件称号的校验。
-
怎么判别资源是否紧缩包,以及怎么解紧缩?
现在简略的选用后缀名是否为.zip判别,运用运用Java内置java.util.zip包下东西解压。
-
怎么校验解压后的资源子文件,避免被篡改?
DynamicPkgInfo同样包括了zip包中一切子文件的校验信息,咱们利用它,来校验一切解压后的文件。
资源运用
- 字体资源运用,从加载好的本地文件中,创立体系Typeface字体方针,并设置到TextView上。
- 帧动画资源运用,从加载好的本地文件中,创立体系AnimationDrawable帧动画方针,并设置到ImageView上。
- 字体和帧动画资源的运用流程,见第5章,内置资源运用流程。
- so资源运用流程,见第7章,so资源加载和运用处理计划。
- 自界说资源的运用,需求sdk运用者自己界说。
-
资源打包
咱们运用dynamic_plugin gradle插件来完结一切资源的打包。
-
字体资源打包
- 扫描输入目录字体文件,将他们复制到输出目录。
- 为每个字体生成一个DynamicPkgInfo类的常量,代表该动态资源。
-
帧动画资源打包
- 扫描输入目录帧动画文件夹,将它们逐一紧缩,并将紧缩包输出到指定目录。
- 为每一组帧动画生成一个DynamicPkgInfo类的常量,代表该动态资源。
-
so资源打包
- Hook体系System.loadLibrary办法的调用。
- 体系打包流程中,删去装备文件指定so文件,并将他们复制到指定目录。
- 扫描上面的so文件目录,将他们逐一紧缩,并将紧缩包输出到指定目录。
- 为每一个so紧缩包出产一个DynamicPkgInfo类的常量,代表该动态资源。
-
自界说资源打包
- 单个文件的资源打包同字体资源
- 多个文件的资源打包同帧动画资源
-
运转产物
- 下图为该打包插件运转一次之后的产物。
- input目录,一切待打包资源的寄存目录,咱们需求手动把要打包的资源复制这里,例如字体文件复制到input/typeface目录下。留意so资源会在打包进程中,主动生成,无需手动处理。
- output目录,则是打包出来的产物,包括字体资源,so资源,帧动画资源等,咱们能够手动将此目录下的打包后资源上传到服务器。
- DynamicResConst.java文件,该文件中生成了一切资源的信息。
- DynamicResConst.java文件的内容,咱们在这里也稍微看一下,图中为字体资源和帧动画资源的java描绘。能够看到一切动态资源,都用DynamicPkgInfo类来描绘。
- 单个文件资源,包括了资源的id,文件称号,资源类型,下载地址,版别号,文件长度以及md5码。
- 多个文件资源,除了包括上述信息外。还包括了该紧缩包解压后,里面每个文件的称号,文件长度以及md5码
-
全体架构
由于整个体系功用较凌乱,咱们将其分为3个module。
- dynamic_res_base:只包括md5,紧缩解压等通用操作以及代表资源的实体类DynamicPkgInfo,该module为后边2个module的基础。
- dynamic_res_core:供给了资源的加载和运用功用,现在包括字体资源,帧动画资源,so资源以及自界说资源。
- dynamic_res_plugin:为一个gradle plugin工程,供给了资源打包功用。
-
dynamic_res_core模块架构
该库包括了动态资源加载和运用全进程,咱们分为5层完结
- 外部接口层,首要为加载办理器和加载监听器,供给了一切外部的接口。
- 资源运用层,封装了几种内置动态资源的运用,字体资源,帧动画资源,so资源。
- 加载流程层,详细完结了资源的加载进程,首要选用状况形式完结,包括一个状况办理器,以及各种状况,例如查看本地版别状况,下载状况,校验文件状况等。
- 接口阻隔层,首要是一些功用接口,例如下载功用,解紧缩功用,上报功用等,阻隔了底层完结。
- 详细完结层,各个详细功用的完结,例如数据库操作,java zip库等。
-
dynamic_res_plugin插件架构
- 体系插件层,首要为体系gradle plugin的完结,以及对dynamic_plugin.gradle装备文件的读取和解析
- 使命模块层,包括了各个使命,例如删去并复制so文件使命,紧缩zip包使命等。
- 底层完结层,包括了详细功用的完结,例如asm结构和transform api,zip紧缩,javepoet代码生成等。
-
通用资源加载,内置资源运用流程
-
通用资源加载主流程
加载一般资源的主流程如下,首要判别资源包指定版别号和本地数据库版别号是否相同,假设想同,进入本地资源校验流程,不然进入下载流程。
-
下载校验解压流程
- 咱们首要调用下载接口下载资源。
- 假设下载成功,咱们校验下载文件,下载失利,则测验删去文件,并直接跳到失利成果。
- 校验下载文件成功,咱们在判别是否为zip文件,对于zip文件,咱们履行解紧缩操作,非zip文件,直接成功。
- 解紧缩完结后,咱们在对解压后的一切文件履行校验操作。
-
本地资源校验流程
- 对于下载并解压的紧缩包资源,以及本地数据库版别和资源实体类版别号相同的资源,咱们需求进行本地资源校验流程。
- 遍历资源包指定的字文件列表,对他们进行逐一文件查验就能够了。
-
单个文件校验流程
资源实体类中指定的文件称号,文件长度,文件md5码和本地文件相一起,咱们以为该文件校验成功了
-
加载康复流程
动态资源加载进程中,或许由于各种原因,导致加载未能得到成功或许失利的成果,而在中间状况被中止,如运用进程被杀死,手机关机等等。为了避免加载意外中止的状况下,彻底从头开端进行加载,咱们设计了一个动态资源加载的康复流程,假设反常中止,咱们下次加载资源时,能够康复到当时状况,继续进行加载。
- 下载进程的康复和断点续传,需求下载接口的完结者担任。
- 其他状况,咱们在状况改动时,将资源id,当时状况和待处理文件途径,保存到数据库。
- 每次加载动态开端时,依据资源id查找数据库中是否有待康复数据。
- 有待康复数据,转到待康复的状况,不然,直接去查看版别号状况。
- 资源加载成功或许失利时,从数据库中删去当时资源id对应的康复状况。
-
内置资源运用流程
前面咱们总结了动态资源的加载流程,资源加载完结后,咱们还需求将该资源进行运用,而这里咱们要说的便是将动态资源运用到对应View上的流程。
- 依据资源id,从缓存中获取动态资源对应的本地文件。
- 文件获取成功,直接设置到view上,获取失利,进入下一步。
- 参数列表中的占位资源不为空,则将占位资源设置到View上。
- 将资源id设置到View的tag上,测验清除上次动态资源加载失利状况。
- 运用办理器Manager类的load办法,履行之前的加载流程。
- 异步等候加载完结回调,判别资源id是否和View的tag相同,避免view被复用,导致的资源错乱状况。
- 假设Activity没有被毁掉,则将资源设置到View上。
-
dynamic_res_core模块类设计
可与第4章,全体架构分层图对照着看
-
外部接口层
DynamicResManager类担任和外部交互,供给了初始化(init),加载资源(load),isResReady(判别资源是否安排妥当),clearFailState(清除错误状况等办法)等办法。
Config类,则能够向办理器供给线程池,下载器接口,本地资源信息接口,本地资源状况接口等装备信息。
AbsResInfo抽象类,代表动态资源。
DynamicPkgInfo类,AbsResInfo的子类,供给给外部运用,代表了一个动态资源实体。
DynamicPkgInfo.FileInfo,AbsResInfo的子类,资源实体内部类,代表了资源中的一个子文件。
DynamicPkgInfo.FolderInfo,AbsResInfo的子类,资源实体内部类,代表了资源中的一个子文件夹。
ILoadResListener接口,供给了加载资源时的回调功用,会回调加载成功,失利,状况改动,下载中进展
-
资源运用层
AbsResApply抽象类,完结了动态资源在ui元素上的运用。
TypefaceResApply类,AbsResApply的子类,代表了字体资源的运用。
FrameAnimApply类,AbsResApply的子类,代表了帧动画资源的运用。
AbsSoLoad抽象类,完结了so动态资源的运用。
RelinkerSoLoad类,AbsSoLoad的子类,运用Relinker第三方库终究load so库。
SystemSoLoad类,AbsSoLoad的子类,运用体系System.loadLibrary办法终究load so库
-
加载流程层
咱们运用状况形式来操控整个动态资源的加载流程。
IState,状况接口,代表了加载流程中的一个状况。
InitState类,初始化状况。
CheckVersionState类,查看资源实体类版别号与数据库版别号是否相同状况。
DownloadState类,下载资源状况。
VerrifyFileState,校验下载资源状况。
UnZipState,解紧缩下载资源状况。
VerifyZipState,校验解压后的一切文件状况。
IStateMechine,状况办理机接口,担任办理前面一切的IState方针。
DefaultStateMachine类,状况办理机的默许完结。
ResCtx类,状况办理机运转进程中的大局context方针,存储了途径信息,加载成功信息,加载失利反常等大局信息。
-
接口阻隔和详细完结层
这2层的类,较为凌乱,限于篇幅,咱们就不一一列举了。
-
类uml图
-
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_res_plugin的android plugin工程,内部包括了多个gradle task。关于as中新建插件的办法,请自行查找其他博客,本文由于篇幅问题,不进行解说。
在咱们的dynamic_res_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_res_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_res_plugin插件流程
-
全体流程
前面咱们现已剖析了通用资源加载,内置资源运用,完结了动态资源办理体系的首要部分。只剩下资源打包部分了,而一切资源的打包操作,都由dyanmic_res_plugin插件来完结。为了完结打包功用,咱们决议在这个dynamic_res_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_res_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文件的动态化。
-
参阅文献
www.jianshu.com/p/260137fdf…
mp.weixin.qq.com/s/X58fK02im…