Android包体积优化计划-动态资源办理体系

动态资源办理体系是货拉拉现在运用的用于办理离线so、动画资源、字体文件的组件,对于减小包体积起着重要作用。详细运用办法参阅开源代码中介绍(github.com/HuolalaTech… )

  1. 前语

  • 随着公司业务的扩展,货拉拉用户端apk包的体积也不断变大,曩昔一年,用户端android组进行了很多的减肥作业,取得了较为明显的效果。再运用常规办法,,现已很难优化包体积了。
  • 咱们能够把一些运用频率相对较低的资源不打包进apk,并在需求时下载到本地(例如动画文件,字体,zip紧缩包,so库等)
  • 咱们留意到,货拉拉用户端apk中,运用了35个以上的so库,而且都支撑arm64-v8a和armeabi-v7a这2种abi,成果便是so体积成倍上涨。用户端出产环境下的apk,解紧缩后,寄存so包的lib目录,占据了整个运用41%的大小。
  • 因而动态资源办理体系是下一个优化的要点,动画,字体和zip包仅仅一般文件,彻底能够支撑动态下载并运用。而so文件本质上便是一种可动态加载并履行的文件,将 so文件动态下发是切实可行的,可是要将它从 apk中除掉并确保稳定性并不是一件易事。
  1. 行业计划

  • 未找到现成的github项目或许三方sdk计划,来完结动态资源办理。
  • 部分博客供给了动态办理so文件的思路,可是短少完整流程。
  • 行业现在并未供给完整的成熟计划供咱们运用,需求咱们自己造轮子。
  1. 功用和计划

  • 完结功用

  • 资源分类,预界说了字体,帧动画,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文件,该文件中生成了一切资源的信息。

Android包体积优化方案-动态资源管理系统

  • DynamicResConst.java文件的内容,咱们在这里也稍微看一下,图中为字体资源和帧动画资源的java描绘。能够看到一切动态资源,都用DynamicPkgInfo类来描绘。
  • 单个文件资源,包括了资源的id,文件称号,资源类型,下载地址,版别号,文件长度以及md5码。
  • 多个文件资源,除了包括上述信息外。还包括了该紧缩包解压后,里面每个文件的称号,文件长度以及md5码

Android包体积优化方案-动态资源管理系统

  1. 全体架构

由于整个体系功用较凌乱,咱们将其分为3个module。

  • dynamic_res_base:只包括md5,紧缩解压等通用操作以及代表资源的实体类DynamicPkgInfo,该module为后边2个module的基础。
  • dynamic_res_core:供给了资源的加载和运用功用,现在包括字体资源,帧动画资源,so资源以及自界说资源。
  • dynamic_res_plugin:为一个gradle plugin工程,供给了资源打包功用。

Android包体积优化方案-动态资源管理系统

  • dynamic_res_core模块架构

该库包括了动态资源加载和运用全进程,咱们分为5层完结

  • 外部接口层,首要为加载办理器和加载监听器,供给了一切外部的接口。
  • 资源运用层,封装了几种内置动态资源的运用,字体资源,帧动画资源,so资源。
  • 加载流程层,详细完结了资源的加载进程,首要选用状况形式完结,包括一个状况办理器,以及各种状况,例如查看本地版别状况,下载状况,校验文件状况等。
  • 接口阻隔层,首要是一些功用接口,例如下载功用,解紧缩功用,上报功用等,阻隔了底层完结。
  • 详细完结层,各个详细功用的完结,例如数据库操作,java zip库等。

Android包体积优化方案-动态资源管理系统

  • dynamic_res_plugin插件架构

  • 体系插件层,首要为体系gradle plugin的完结,以及对dynamic_plugin.gradle装备文件的读取和解析
  • 使命模块层,包括了各个使命,例如删去并复制so文件使命,紧缩zip包使命等。
  • 底层完结层,包括了详细功用的完结,例如asm结构和transform api,zip紧缩,javepoet代码生成等。

Android包体积优化方案-动态资源管理系统

  1. 通用资源加载,内置资源运用流程

  • 通用资源加载主流程

加载一般资源的主流程如下,首要判别资源包指定版别号和本地数据库版别号是否相同,假设想同,进入本地资源校验流程,不然进入下载流程。

Android包体积优化方案-动态资源管理系统

  • 下载校验解压流程

    • 咱们首要调用下载接口下载资源。
    • 假设下载成功,咱们校验下载文件,下载失利,则测验删去文件,并直接跳到失利成果。
    • 校验下载文件成功,咱们在判别是否为zip文件,对于zip文件,咱们履行解紧缩操作,非zip文件,直接成功。
    • 解紧缩完结后,咱们在对解压后的一切文件履行校验操作。

Android包体积优化方案-动态资源管理系统

  • 本地资源校验流程

    • 对于下载并解压的紧缩包资源,以及本地数据库版别和资源实体类版别号相同的资源,咱们需求进行本地资源校验流程。
    • 遍历资源包指定的字文件列表,对他们进行逐一文件查验就能够了。

Android包体积优化方案-动态资源管理系统

  • 单个文件校验流程

资源实体类中指定的文件称号,文件长度,文件md5码和本地文件相一起,咱们以为该文件校验成功了

Android包体积优化方案-动态资源管理系统

  • 加载康复流程

动态资源加载进程中,或许由于各种原因,导致加载未能得到成功或许失利的成果,而在中间状况被中止,如运用进程被杀死,手机关机等等。为了避免加载意外中止的状况下,彻底从头开端进行加载,咱们设计了一个动态资源加载的康复流程,假设反常中止,咱们下次加载资源时,能够康复到当时状况,继续进行加载。

  • 下载进程的康复和断点续传,需求下载接口的完结者担任。
  • 其他状况,咱们在状况改动时,将资源id,当时状况和待处理文件途径,保存到数据库。
  • 每次加载动态开端时,依据资源id查找数据库中是否有待康复数据。
  • 有待康复数据,转到待康复的状况,不然,直接去查看版别号状况。
  • 资源加载成功或许失利时,从数据库中删去当时资源id对应的康复状况。

Android包体积优化方案-动态资源管理系统

  • 内置资源运用流程

前面咱们总结了动态资源的加载流程,资源加载完结后,咱们还需求将该资源进行运用,而这里咱们要说的便是将动态资源运用到对应View上的流程。

  • 依据资源id,从缓存中获取动态资源对应的本地文件。
  • 文件获取成功,直接设置到view上,获取失利,进入下一步。
  • 参数列表中的占位资源不为空,则将占位资源设置到View上。
  • 将资源id设置到View的tag上,测验清除上次动态资源加载失利状况。
  • 运用办理器Manager类的load办法,履行之前的加载流程。
  • 异步等候加载完结回调,判别资源id是否和View的tag相同,避免view被复用,导致的资源错乱状况。
  • 假设Activity没有被毁掉,则将资源设置到View上。

Android包体积优化方案-动态资源管理系统

  1. 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图

Android包体积优化方案-动态资源管理系统

  1. 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的使命呢?

Android包体积优化方案-动态资源管理系统

看一眼这幅超级凌乱的apk构建流程图,嗯,能够看到,体系的确会在apkBuilder构建前,将本地的c/c++文件编译成so库,并将第三方的so库一起打包到apk中,咱们需求寻找的便是搜集一切so库的体系Task

Android包体积优化方案-动态资源管理系统

经过查找资料,咱们发现,的确有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文件。

Android包体积优化方案-动态资源管理系统

详细履行替换的代码如下,在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的方针。

  1. 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 可视化界面,十分适宜快速上手。

Android包体积优化方案-动态资源管理系统

  1. so动态化流程

  • so资源运用流程

  • 获取体系支撑abi列表,依据该列表,找到适宜的so动态资源实体类。
  • 假设该资源现已被加载缓存,则直接回调加载成功。
  • 不然,开端资源通用加载流程,并异步等候资源加载成功(流程见第5章)。
  • 再次判别下载校验后的资源,是否支撑本机abi。
  • 将so包途径参加DexPathList的数组头部。
  • 遍历等候加载so列表,测验加载一切so文件,并将成功加载的so文件,移除该列表。
  • 将资源id和本地途径参加缓存,避免so被重复加载。
  • 回调加载完结监听器。

Android包体积优化方案-动态资源管理系统

  • SoLoadUtil.loadLibrary办法流程

从上一章咱们知道,咱们会运用transform api加asm结构,将体系的System.loadLibrary办法替换成咱们的SoloadUtil.loadLibrary办法。咱们替换体系办法的意图。一个是为了确保so库不存在时,程序不溃散,别的一个便是so库下载校验完结后,能主动完结之前失利的加载,为此,咱们设计了如下流程。

  • 其他办法调用到咱们的SoloadUtil时,咱们判别咱们的加载体系是否初始化完结
  • 已完结,则调用Relinkder库测验加载so文件,未完结则将该so库参加待加载行列中。
  • 假设Relinker加载so文件成功,咱们从待加载行列中移除so,而且完结本次加载。
  • 不然咱们依然将so文件参加待加载行列中。
  • 依据上面的so加载流程,当so动态资源真实下载校验完结后,咱们会遍历待加载行列,并完结一切之前未成功的so库加载。

Android包体积优化方案-动态资源管理系统

  1. 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参加使命行列。
  • 启动使命行列。

Android包体积优化方案-动态资源管理系统

  • TransformTask流程

该task流程,首要便是经过tranform api和asm结构的运用,咱们在其中参加了扫描class范围的可装备项。

  • 等候asm结构扫描class。
  • 判别该class称号是否在咱们装备的替换列表中,假设不在,就直接回来。
  • 创立ClassVisitor和MethodVisitor,等候asm结构扫描每个办法。
  • 假设该办法的称号,参数列表和调用者,都和System.loadLibrary办法相符合。
  • 咱们替换为自己的SoloadUtil.loadLibrary办法。

Android包体积优化方案-动态资源管理系统

  • DeleteAndCopySoTask流程

  • 依据装备文件,找到体系的merge和strip task。
  • 将咱们的task刺进到2个体系task之间,并等候体系回调咱们的doLast办法。
  • 遍历体系的mergeTask的输出目录,判别该so文件是否在咱们装备的待扫描列表中。
  • 假设装备了需求复制so文件,则咱们将它复制到指定位置。
  • 假设装备了需求删去so文件,则咱们将该so文件删去。

Android包体积优化方案-动态资源管理系统

  • ZipResTask流程

  • 复制字体文件,将文件信息参加资源列表。
  • 紧缩帧动画文件,将紧缩后的文件信息参加资源列表。
  • 紧缩so文件,将紧缩后的文件信息参加资源列表。
  • 紧缩zip文件夹下文件,将紧缩后的文件信息参加资源列表。
  • 遍历资源文件,为其生成相应的资源实体类DynamicPkgInfo。

Android包体积优化方案-动态资源管理系统

  1. 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图

Android包体积优化方案-动态资源管理系统

  1. 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'
 }
  1. 优化效果

经过引进动态资源办理体系,并将一键报警sdk相关的so文件和其他一般资源动态化后,货拉拉用户端的包体积减少了8M,从54M变为了46M。后继将会继续测验进行其他so文件的动态化。

Android包体积优化方案-动态资源管理系统

  1. 参阅文献

www.jianshu.com/p/260137fdf…

mp.weixin.qq.com/s/X58fK02im…