Matrix源码分析系列-如何解析应用安装包(一)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Matrix源码分析系列-如何解析应用安装包

前言


首先我们思考一个问题,为什么要解析安装包?目的是什么?什么原因促使我们做这件事?

  • 减小包的体积,产品或运营同学认为,包体积越小,越能提高下载量
  • 应用市场限制,如App Store、Google Play 都有相关包体积的规定,都是以更小为主
  • 减少内存占用,不管是Rom还是Ram 肯定是随着应用包的体积增加而成正比增加,所以减小包体,也是在间接优化内存占用

我们找了这么几个原因,促使我们做这件事,确实在实际的工作中,特别是toC的小伙伴,更加明显,他们一直在做包体积的优化,那么要优化体积,那肯定是要知道包的构成,这样才能有合适的优化方法,那么我们再来看看包的构成。

包的构成分析


我们利用Android studio的可视化工具,打开了一个普通的apk安装包,如下

image.png

为了更有说服力,我拿了WX的包来分析看,如下:

image.png

通过两张图,我们分辨出包的大致构成如下:

  • Dex、so库。且WX的SO库只保留了armeabi-v7a架构的包,已占比50%,可见很高。
  • r资源文件及assets ,存放图片,音频,资源文件的位置
  • resources.arsc 文件也达到了6.8MB,这是资源索引表,开发中Resources就是通过resources.arsc把Resource的ID转化成资源文件的名称,然后交由AssetManager来加载的,它是由AAPT工具在打包过程中生成。
  • META-INF  签名信息
  • AndroidManifest.xml  清单配置文件

整体来看,占比较高的就是 Dex、So、r、assets、resources.arsc,那么我们优化,肯定是要从这几个方面入手对吧。

如何减小安装包的体积


1.资源压缩


对大图进行无损压缩,对不需要alpha通道的png图,压缩成jpg,或者使用更小的webp图片:webp介绍

对于assets中存储的音频文件可以选择远程依赖,第一次下载后做缓存处理

2.通过编译器缩减,混淆


利用R8 编译器,进行代码缩减、资源缩减、混淆处理等,都可以有效的减小包体积,具体介绍请看:缩减、混淆处理和优化应用

注意:R8编译器要求 Android Gradle 插件 3.4.0 或更高版本

3.resources.arsc文件缩减


这个文件怎么缩减呢?经过查询资料发现,该文件对于不同的语言,不同的编码格式有一定的影响,直接说结论:

  • 对于纯英文来讲,建议使用utf-8格式编码
  • 对于中文来讲,建议使用utf-16

具体实现操作请看: aapt 相关命令

4.so库精简


通过上面第二张图我们发现,wx的so库只保留了armeabi-v7a架构,这也是目前最流行的架构,wx这么大的用户量都敢只保留一个,你有啥不敢的。将x86、arm64-v8a果断删了吧。这是表面的优化,更深层次的就需要对so库代码精简,如:抽离独立的库,减少冗余代码。还有建议C++运行时库使用stlport_shared,同样可以减少包大小,且可以节省一点内存,这种方式请注意:应用程序需要先加载所需要的共享库,然后再加载依赖此共享库的其他原生模块

static{
     System.loadLibrary("stlport_shared");
     System.loadLibrary("xxxxx");
}

5.Dex文件数量优化


在我们使用multiDex后,或者说方法数达到65535之后,不得不对代码进行分包,分包会带来什么问题呢?

  • method id 分配不合理导致更多的Dex量,由于method id 的大量冗余导致每个 Dex 真正可以放的 Class 变少。
  • 信息存储冗余,因为每个dex中都存在调用的方法的详细信息,举个例子,如果一个class method被其他dex引用到的话就会导致 这个class不光是在自己的dex中存在方法信息,被引用到的dex中也存储了class的方法信息,这样造成冗余,冗余过多就会导致dex数量增加。

知道了问题如何解决呢?答案就是尽量让方法的引用都在同一个dex中,这样就可以减少冗余,减少dex的新增,目前最优解建议使用:Facebook 的一个开源编译工具ReDex,具体使用方法建议去看文档:github.com/facebook/re…,这里就不展开描述。

6.Dex压缩


此方法还是来源于Facebook的包,它真正的dex代码放到了assets目录,且通过xz 压缩算法(该算法压缩率比 Zip 高 30% 左右),并通过应用首次启动的时候解压缩,并利用多线程解压缩方式,耗时并没有那么明显。

小结


说了这么多的优化放法,如果想做到极致,肯定还有方法,但我们现在处于5G时代,大家还会对10M甚至说100M有感觉吗?这就需要于用户体验之间做一个权衡,一些极致的优化肯定是会降低用户体验的,需要按需而行吧。

Matrix App Checker


终于进入正题,我们了解了包的结构和常见的缩减方法,那么App Checker到底可以为我们提供什么样的帮助,来辅助我们进行缩减呢?随我一探究竟。

代码目录


image.png

可以看到libs中引入三方jar包- apktool-lib-2.4.0.jar , 它的作用就是将apk反编译出来,产出dex、libs、manifest等文件。再往下看代码

  • exception目录中 抽象了两个 TaskExecuteException、TaskInitException异常,任务执行和任务初始化异常,方便捕获。
  • job目录中 抽象出 ApkJob 来管理所有的 Task 任务和 JobResult
  • output 目录 主要作用就是将输出的结果 以Json或者html格式的方式写入文件中
  • result 目录 对JobResult、TaskResult的抽象及相关实现
  • task 目录 所有任务的实现,包括 CountClassTask 统计类数量、MethodCountTask 统计方法量、UnzipTask 解压任务负责将apk解压成相关文件。
  • ApkChecker 负责创建Job,然后调用run方法。

类图


文字描述总显得有些乏力,画一下类结构图来帮助我们理解代码。

image.png

ApkChecker是检查启动类,负责创建出ApkJob,再由ApkJob创建出任务列表,通过Factory工厂模式创建出实际的任务对象,最后返回任务结果,大致就是这样,所以大致的流程图应该是这样。

细看源码


ApkChecker部分源码:

public final class ApkChecker {
    public static void main(String... args) {
        if (args.length > 0) {
            //创建出ApkChecker对象
            ApkChecker m = new ApkChecker();
            m.run(args);
        } else {
            System.out.println(INTRODUCT + HELP);
            System.exit(0);
        }
    }
   private void run(String[] args) {
       // 创建Job对象,调用run函数
      ApkJob job = new ApkJob(args);
      try {
          job.run();
      } catch (Exception e) {
          e.printStackTrace();
          System.exit(-1);
      }
   }
}

接着往下看ApkJob的run 方法:

public void run() throws  Exception {
        //解析参数,创建对应的task
        if (parseParams()) {
            //创建解压任务对apk进行解压操作
            ApkTask unzipTask = TaskFactory.factory(TaskFactory.TASK_TYPE_UNZIP, jobConfig, new HashMap<String, String>());
            // 将任务放入preTasks集合中
            preTasks.add(unzipTask);
            //通过jobConfig获取 输出格式配置,
            for (String format : jobConfig.getOutputFormatList()) {
                //获取对应format的对象JobJsonResult或JobHtmlResult
                JobResult result = JobResultFactory.factory(format, jobConfig);
                if (result != null) {
                    jobResults.add(result);
                } else {
                    Log.w(TAG, "Unknown output format name '%s' !", format);
                }
            }
            //开始执行
            execute();
        } else {
            ApkChecker.printHelp();
        }
    }
// 该函数作用就是根据命令行传递的参数,创建对应的Task,最后将其放入taskList集合中
  private boolean parseParams() {
        if (args != null && args.length >= 2) {
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80454d55247f41b1a2db8e8fe3d5d16b~tplv-k3u1fbpfcp-watermark.image)
            int paramLen = parseGlobalParams();
            for (int i = paramLen; i < args.length; i++) {
                if (args[i].startsWith("-") && !args[i].startsWith("--")) {
                    Map<String, String> params = new HashMap<>();
                    paramLen = parseParams(i + 1, args, params);
                    if (!params.containsKey(JobConstants.PARAM_R_TXT)) {
                        String inputDir = jobConfig.getInputDir();
                        if (!Util.isNullOrNil(inputDir)) {
                            params.put(JobConstants.PARAM_R_TXT, inputDir + "/" + ApkConstants.DEFAULT_RTXT_FILENAME);
                        }
                    }
                    ApkTask task = createTask(args[i], params);
                    if (task != null) {
                        taskList.add(task);
                    }
                    i += paramLen;
                }
            }
        } else {
            return false;
        }
        return true;
    }
// 最后看执行的过程
private void execute() throws Exception {
        try {
      //首先将 preTasks 准备中的任务 优先执行,然后将执行的结果
            for (ApkTask preTask : preTasks) {
                //任务初始化
                preTask.init();
                //任务同步执行,并获取结果
                TaskResult taskResult = preTask.call();
                if (taskResult != null) {
                    TaskResult formatResult = null;
                    //遍历预先配置好的结果集
                    for (JobResult jobResult : jobResults) {
                        //根据taskResult 与 jobResult 配置匹配结果
                        formatResult = TaskResultFactory.transferTaskResult(taskResult.taskType, taskResult, jobResult.getFormat(), jobConfig);
                        if (formatResult != null) {
                            //将解压的任务结果放到jobResult中
                            jobResult.addTaskResult(formatResult);
                        }
                    }
                }
            }
            //初始化其他任务
            for (ApkTask task : taskList) {
                task.init();
            }
            //解压缩还是同步执行,这里就用到了异步,创建一个线程池,通过代码发现,默认只有一个线程。
            List<Future<TaskResult>> futures = executor.invokeAll(taskList, timeoutSeconds, TimeUnit.SECONDS);
            for (Future<TaskResult> future : futures) {
                //通过get同步获取返回结果,由于线程池只有一个线程,所以看似并发,其实这里并没有。
                TaskResult taskResult = future.get();
                if (taskResult != null) {
                    TaskResult formatResult = null;
                    for (JobResult jobResult : jobResults) {
                        //同样匹配结果最终放入到jobResult中
                        formatResult = TaskResultFactory.transferTaskResult(taskResult.taskType, taskResult, jobResult.getFormat(), jobConfig);
                        if (formatResult != null) {
                            jobResult.addTaskResult(formatResult);
                        }
                    }
                }
            }
            //关闭线程池
            executor.shutdownNow();
            for (JobResult jobResult : jobResults) {
                //输出到文件中
                jobResult.output();
            }
            Log.d(TAG, "parse apk end, try to delete tmp un zip files");
            //删除解压的apk文件目录
            FileUtils.deleteDirectory(new File(jobConfig.getUnzipPath()));
        } catch (Exception e) {
            Log.e(TAG, "Task executor execute with error:" + e.getMessage());
            throw e;
        }
    }

整个过程,其实很简单,并不复杂,整个流程走完了,我们缺不知,导致任务执行了什么样子的代码,才能获取到相应的信息呢?我们来看几个具体的Task任务

MethodCountTask

为什么选择方法计数呢?因为我们经常会遇到项目方法数量统计需求,到底是如何统计的呢?我们是不是可以借助这次的学习就可以搞定了?随我来。直接上源码

//首先看下初始化方法,都做了哪些事情。
    @Override
    public void init() throws TaskInitException {
        super.init();
        // 获取解压后的apk文件目录
        String inputPath = config.getUnzipPath();
        if (Util.isNullOrNil(inputPath)) {
            throw new TaskInitException(TAG + "---APK-UNZIP-PATH can not be null!");
        }
        Log.i(TAG, "input path:%s", inputPath);
        // 根据路创建File对象,检查文件的属性,看是否匹配规则
        inputFile = new File(inputPath);
        if (!inputFile.exists()) {
            throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not exist!");
        } else if (!inputFile.isDirectory()) {
            throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not directory!");
        }
        // 如果匹配规则,就找到文件夹下的所有文件
        File[] files = inputFile.listFiles();
        try {
            if (files != null) {
                for (File file : files) {
                    //找到dex结尾的文件
                    if (file.isFile() && file.getName().endsWith(ApkConstants.DEX_FILE_SUFFIX)) {
                        // 加入到dexFileNameList列表缓存中
                        dexFileNameList.add(file.getName());
                        //RandomAccessFile是Java中输入,输出流体系中功能最丰富的文件内容访问类
                        //它提供很多方法来操作文件,包括读写支持
                        //与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序可以直接跳到任意地方来读写数据。
                        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
                        //随机文件对象加入到dexFileList中
                        dexFileList.add(randomAccessFile);
                    }
                }
            }
        } catch (FileNotFoundException e) {
            throw new TaskInitException(e.getMessage(), e);
        }
        //获取配置信息,目的是为了分组,是按类 class 或者按包 package
        if (params.containsKey(JobConstants.PARAM_GROUP)) {
            if (JobConstants.GROUP_PACKAGE.equals(params.get(JobConstants.PARAM_GROUP))) {
                group = JobConstants.GROUP_PACKAGE;
            } else if (JobConstants.GROUP_CLASS.equals(params.get(JobConstants.PARAM_GROUP))) {
                group = JobConstants.GROUP_CLASS;
            } else {
                Log.e(TAG, "GROUP-BY '" + params.get(JobConstants.PARAM_GROUP) + "' is not correct!");
            }
        }
    }


目录
相关文章
|
4天前
|
缓存 Kubernetes Docker
GitLab Runner 全面解析:Kubernetes 环境下的应用
GitLab Runner 是 GitLab CI/CD 的核心组件,负责执行由 `.gitlab-ci.yml` 定义的任务。它支持多种执行方式(如 Shell、Docker、Kubernetes),可在不同环境中运行作业。本文详细介绍了 GitLab Runner 的基本概念、功能特点及使用方法,重点探讨了流水线缓存(以 Python 项目为例)和构建镜像的应用,特别是在 Kubernetes 环境中的配置与优化。通过合理配置缓存和镜像构建,能够显著提升 CI/CD 流水线的效率和可靠性,助力开发团队实现持续集成与交付的目标。
|
27天前
|
机器学习/深度学习 人工智能 自然语言处理
AI技术深度解析:从基础到应用的全面介绍
人工智能(AI)技术的迅猛发展,正在深刻改变着我们的生活和工作方式。从自然语言处理(NLP)到机器学习,从神经网络到大型语言模型(LLM),AI技术的每一次进步都带来了前所未有的机遇和挑战。本文将从背景、历史、业务场景、Python代码示例、流程图以及如何上手等多个方面,对AI技术中的关键组件进行深度解析,为读者呈现一个全面而深入的AI技术世界。
112 10
|
1天前
|
供应链 搜索推荐 API
深度解析1688 API对电商的影响与实战应用
在全球电子商务迅猛发展的背景下,1688作为知名的B2B电商平台,为中小企业提供商品批发、分销、供应链管理等一站式服务,并通过开放的API接口,为开发者和电商企业提供数据资源和功能支持。本文将深入解析1688 API的功能(如商品搜索、详情、订单管理等)、应用场景(如商品展示、搜索优化、交易管理和用户行为分析)、收益分析(如流量增长、销售提升、库存优化和成本降低)及实际案例,帮助电商从业者提升运营效率和商业收益。
30 17
|
19天前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
17天前
|
安全 API 数据安全/隐私保护
速卖通AliExpress商品详情API接口深度解析与实战应用
速卖通(AliExpress)作为全球化电商的重要平台,提供了丰富的商品资源和便捷的购物体验。为了提升用户体验和优化商品管理,速卖通开放了API接口,其中商品详情API尤为关键。本文介绍如何获取API密钥、调用商品详情API接口,并处理API响应数据,帮助开发者和商家高效利用这些工具。通过合理规划API调用策略和确保合法合规使用,开发者可以更好地获取商品信息,优化管理和营销策略。
|
2月前
|
存储 监控 API
深入解析微服务架构及其在现代应用中的实践
深入解析微服务架构及其在现代应用中的实践
78 12
|
1月前
|
机器学习/深度学习 搜索推荐 API
淘宝/天猫按图搜索(拍立淘)API的深度解析与应用实践
在数字化时代,电商行业迅速发展,个性化、便捷性和高效性成为消费者新需求。淘宝/天猫推出的拍立淘API,利用图像识别技术,提供精准的购物搜索体验。本文深入探讨其原理、优势、应用场景及实现方法,助力电商技术和用户体验提升。
|
2月前
|
编译器 PHP 开发者
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
36 1
|
2月前
|
监控 网络协议 算法
OSPFv2与OSPFv3的区别:全面解析与应用场景
OSPFv2与OSPFv3的区别:全面解析与应用场景
54 0
|
2月前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
69 0

推荐镜像

更多