以libaray维度分析包体

1 背景

最近贝壳APP一直在做瘦身,需要对包体大小进行分析,但刚开始Android只能分析出apk中包含哪些文件,并不知道文件来源于哪个library,更不知道library对应的维护组,那么APK瘦身项目在实施时就没有一个明确的责任划分。经过调研发现Android业内还没有一个包体按照library维度进行分析的方案,比较有名的是腾讯开源的matrix,它也只能做到按照文件类型进行包体分析,并不能到按照library的维度进行分析整个包体。

2 apk文件结构

做包体分析之前,我们先了解一下apk文件的结构,其实就是一个压缩文件,大概结构如下:

apk结构.jpg

assert目录:存放我们app/src/main/assets目录下的资源文件,另外flutter的资源文件也会放在该目录下

res目录:存放我们项目的资源文件,例如:图片,xml布局,values.xml和音频等资源

lib目录:存放我们项目中所有的so文件

.dex:所有的java代码先会通过javac命令编译成.class文件,然后通过dx工具转成dex文件

resources.arsc:资源映射表,通过AndroidStudio我们可以看到apk文件中资源文件类型,id,名称和资源所在目录

其它:主要是一些java库下的META-INFO目录下的文件。

资源表.png

我们都知道Android打包的过程中,会把Android工程和它引用的library的代码和资源进行校验和merge,如果发生冲突那么将会打包失败,如果打包成功的话将会生成我们的apk文件,那么在这个打包过程中会不会存在一些中间文件,来记录工程中library和它包含的文件之间的映射关系呢?

通过查看所有app/build/目录下的所有文件,发现确实存在这个中间文件,记录着library下有有哪些res资源,asset资源,so文件,META-INFO文件。那么在打包过程中我们可以收集这些中间文件,并上传到我们的maven;打包完成之后触发我们的分析job,先拿到apk文件中的所有文件,然后解析收集上来的这些中间文件,这样就可以实现按照library的维度来进行包大小分析了。


包体分析流程.png

3 解析中间文件

3.1 中间文件位置

这些文件全部存放在app/build/intermediates/incremental目录中:

res资源合并的映射文件: app/build/intermediates/incremental/mergeDebugResources/merger.xml
assert资源合并映射表: app/build/intermediates/incremental/mergeDebugAssets/merger.xml
so文件合并映射表:有app/build/intermediates/incremental/a_plusDebug-mergeNativeLibs/merge-state
java资源合并映射表: app/build/intermediates/incremental/debug-mergeJavaRes/merge-state

3.2 收集中间文件

如果文件路径写死,那会存在兼容性问题,通过gradle api获取的这些文件路径的话,那么就不存在兼容性问题了。

def extension_merge_state = project.extensions.getByName("android")
extension_merge_state.applicationVariants.all { variant ->
    def variant_name = variant.name
    //1、res资源合并的映射文件:app/build/intermediates/incremental/mergeDebugResources/merger.xml
    def mergeResourcesTask = variant.getMergeResources()
    mergeResourcesTask.doLast{
        def mergeResFile = mergeResourcesTask.incrementalFolder.absolutePath + "/merger.xml"
        println(mergeResFile)
        appendFilePath("mergeResources.xml",mergeResFile)
    }

    //2、assert资源合并映射表:app/build/intermediates/incremental/mergeDebugAssets/merger.xml
    def mergeAssetsTask = variant.getMergeAssets()
    mergeAssetsTask.doLast{
        def assertMergerFile = mergeAssetsTask.incrementalFolder.absolutePath + "/merger.xml"
        println(assertMergerFile)
        appendFilePath("mergeAssets.xml", assertMergerFile)
    }

    //3、so文件合并映射表,通过序列化IncrementalFileMergerState实现:app/build/intermediates/incremental/a_plusDebug-mergeNativeLibs/merge-state
    def container = variant.variantData.taskManager.taskFactory.taskContainer
    Task mergeNativeLibsTask = container.getByName("merge${variant_name.capitalize()}NativeLibs")
    mergeNativeLibsTask.doLast {
        def cache_merge_state = mergeNativeLibsTask.cacheDir.getParent() + "/merge-state"
        println(cache_merge_state)
        appendFilePath("mergeNativeLibs_merge_state",cache_merge_state)
    }

    //4、java资源合并映射表,通过序列化IncrementalFileMergerState实现 app/build/intermediates/incremental/debug-mergeJavaRes/merge-state
    Task mergeJavaResourceTask = container.getByName("merge${variant_name.capitalize()}JavaResource")
    mergeJavaResourceTask.doLast {
        def cache_merge_state = mergeJavaResourceTask.cacheDir.getParent() + "/merge-state"
        println(cache_merge_state)
        appendFilePath("mergeJavaRes_merge_state", cache_merge_state)
    }
}

def void appendFilePath(file_key, file_path){
    String input_dir = project.buildDir.toPath().toString() + "/merge_state"
    File dir = new File(input_dir)
    if(!dir.exists()){
        dir.mkdir()
    }
    def inputFile = new File(dir.absolutePath,"merge_files.txt")
    if(!inputFile.exists()){
        inputFile.createNewFile()
    }
    inputFile.append("${file_key}:${file_path}\n")
}

在打包过程中,我会给app/build.gradle文件注入上面gradle脚本,这样做的好处是省去app接入的成本。知道这些文件的路径之后,我会通过python打包脚本上传这些文件到maven中。

3.3 中间文件解析

其中merger.xml文件比较好解析,但这个merge-state是个什么文件,通过在命令行中执行 【file merge-state文件路径】命令,发现这个文件是一个持久化java序列化对象产生的文件。

执行file命令.png

那这个文件到底是哪个对象序列化生成的呢,通过用AndroidStudio打开merge-state文件,可以猜到是com.android.builder.merge.IncrementalFileMergerState序列化生成的

解压merge-state文件.png

通过在gradle plugin项目中解析发现,这确实是com.android.builder.merge.IncrementalFileMergerState这个类序列化生成的文件。

那么问题来了,分析是在python环境中进行的,那么我们怎么去解析这个merge-state文件呢?通过了解腾讯matrix的分析包体的方案,

我们可以把解析的代码打成一个jar包,然后通过在python中执行调用这个jar的命令就可以完成解析了。

解析merge_state的代码如下:

public class MergeStateParser {
 public static void main(String[] args) {
 parseObject(args[0],args[1]);
 }
 public static void parseObject(String mergeStatePath, String outputJsonPath){
 ObjectInputStream ois = null;
 try {
 ois = new ObjectInputStream(new FileInputStream(mergeStatePath));
 IncrementalFileMergerState merge_state = (IncrementalFileMergerState) ois.readObject();
 System.out.println(merge_state);
 String json = new Gson().toJson(merge_state);
 FileWriter fw = null;
 try {
 fw = new FileWriter(outputJsonPath);
 fw.write(json);
 fw.flush();
 fw.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
 }catch (Exception ex){
 System.out.println(ex.getMessage());
 } finally {
 try {
 if (ois != null){
 ois.close();
 }
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 }
}

解析的时候还要注意,com.android.builder.merge.IncrementalFileMergerState这个类是在第三方库中的,所以需要添加以下依赖才能打出jar包

  implementation "com.google.guava:guava:27.0.1-jre"

3.4 中间文件内容

merger.xml:dataSet中 config属性的value值为library的信息,每个source下的file文件就是library中的文件。当然这里解析的时候,我们需要过滤一些特殊的字符,才能拿到library的group_id,artifact_id和version。






 
 ...




下面是res资源文件解析后的结果如下:

res资源文件解析结果.png

合并res资源的过程中需要注意:

1、所有library和工程中的values文件夹下的字符串,color这些属性,最终合并成一个文件,这里没有分类,责任划分的时候最终会算到APP维护组下名下。

2、同样所有aar库和工程中的AndroidManifest.xml在打包过程中也会进行合并

merge_state:解析出来的class实例,包含三个字段、两个map和一个List。

/**
 * Names of all inputs to merge, in order.
 */
private final ImmutableList inputNames;
/**
 * Maps OS-independent paths to the names of the input sets that were used to construct the
 * merged output.
 */
private final ImmutableMap> origin;
private final ImmutableMap> byInput;

对我们有用的是byInput字段,结构如下:library产物路径对应着多个文件

image.png

我们在keOnes(持续集成系统)中会注册这些library的信息,通过merge.xml可以拿到library的group_id和artifact_id,通过调用api接口,就可以解析出library在数据库中的名称。但是通过解析merge_state文件,我们可以拿到jar和aar在gradle缓存目录的路径,例如:

~/gradle/gradle-4.1/caches/transforms-2/files-2.1/cd6b3a2f4da4a2ecf7cedbc4998ac5b2/jetified-lib_castscreen-1.1.1
/jars/classes.jar
~/gradle/gradle-4.1/caches/modules-2/files-2.1/com.ke.crashly/collector/1.6.5
/9176c716a002a64fe90bc9787272228b362a5d4a/collector-1.6.5.jar

备注:带有jetified这种一般是aar中class.jar的路径,如果依赖对应的产物是jar包的话,那么就没有jetified。

解析这个路径只能拿到artifact_id和library版本号,通过artifact_id请求keOnes(持续集成系统)接口便可获取到library在数据库中的组件名称。

image.png

4 library与维护组对应关系维护

上面讲到通过解析gradle构建过程中生成的中间文件,可以拿到文件和library的对应关系,但这还不够,如果不知道library是谁维护的,那整个包体分析将没多大价值,为了解决这个问题,我们在keOnes(持续集成系统)中维护了library和library对应的关系,维护界面如下:

image.png

library名称:library名称一般为group_id:artifact_name

dep_name:group_id:artifact_name:{version}@[aar/jar]

针对维护组与library对应关系的维护,我们遵循一个规则:一般情况下,谁维护的library维护组就是谁,对于一些第三方库那么谁引用的就算谁,系统和公共的库属于架构组。

5 该方案在包体分析功能的落地

5.1 业务线包体大小占比

前面我们拿到了res资源、assert资源、so文件和META-INF文件与library对应的关系,那么就好办了,我们可以通过拿到apk下所有的文件,根据这个对应关系就可以分析出在apk文件中library有哪些文件了;同时在keOnes(持续集成系统)我们会记录library和维护组之间的关系,这样我们就知道每个维护组包体大小的占比。目前keOnes(持续集成系统)系统已经上线了包体分析功能,效果如下所示:

包大小分析.png

5.2 大文件分析

apk中文件总共有res资源、assert资源、so文件和其它四个大类,每个大类下面我们会按照文件的后缀进行分类,并且会跟业务的同学沟通并设置一个合理的阈值。目前在keOnes(持续集成系统)的分析效果如下:

大文件分析.png

6 遇到问题

1、通过解析merge-state文件只能分析出library的artifact_id和version,而不能获取到group_id,那么可能会造成在keOnes(持续集成系统)中找出来的library名称会存在多个。

解决办法:构建时收集项目所有runtime依赖,然后再通过artifact_id和version进行匹配,最终找到group_id,这样从keOnes(持续集成系统)获取到的library名称就唯一了。

2、针对放在工程lib目录下的jar和aar,是没有group_id和artifact_id的。

解决办法:针对这种情况,建议把这些依赖库上传到maven,手动地给这些library设定一个group_id和artifact_id,同时也要在keOnes(持续集成系统)注册这些library,这样就工程所有的library都管理起来了。

3、dex文件不能按照library维度继续进行包体的分析了,这对我们的apk瘦身也没有很大的指导意义。但我们可以分析出每个library下有多个类,多少个方法。

4、项目构建统一是在gradle5.4.1进行的,经过测试gradle6.0+也没有问题,但一些低版本的gradle构建的时候,可能会没有这些中间文件。

7 总结

要做到按照library的维度去分析包体,关键在于发现并解析资源和代码合并过程中产生的中间文件。同时也需要注册项目中library和维护组的关系,贝壳B、C两端总注册了400多个library,注册这块工作量还是不小的。有了library和维护组的对应关系之后,后面我们对外还可以提供一个精准的分流服务了,运用场景将覆盖crash精准分流、SNAPSHOT依赖检测、权限检测等。

你可能感兴趣的:(以libaray维度分析包体)