美文网首页
以libaray维度分析包体

以libaray维度分析包体

作者: 砺雪凝霜 | 来源:发表于2021-07-05 15:22 被阅读0次

    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。

    <merge>
    <dataSet config="com.huawei.hms:network-common:4.0.2.300$Generated">
    <source path="~/.gradle/caches/transforms-2/files-2.1/2f0a9e8fc70c40e9075d15cdfac8d50c/network-common-4.0.2.300
    /res">
    <file path="~/.gradle/caches/transforms-2/files-2.1/2f0a9e8fc70c40e9075d15cdfac8d50c/network-common-4.0.2.300
    /res/values/values.xml" qualifiers="">
    </file>
     <file>
     ...
    </file>
    </source>
    </dataSet>
    </merge>
    

    下面是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<String> inputNames;
    /**
     * Maps OS-independent paths to the names of the input sets that were used to construct the
     * merged output.
     */
    private final ImmutableMap<String, ImmutableList<String>> origin;
    private final ImmutableMap<String, ImmutableSet<String>> 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维度分析包体

          本文链接:https://www.haomeiwen.com/subject/fqnscltx.html