前言

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

  • 减小包的体积,产品或运营同学认为,包体积越小,越能提高下载量
  • 应用市场限制,如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,同样可以减少包大小,且可以节省一点内存,这种方式请注意:应用程序需要先加载所需要的共享库,然后再加载依赖此共享库的其他原生模块

  1. static{
  2. System.loadLibrary("stlport_shared");
  3. System.loadLibrary("xxxxx");
  4. }

5.Dex文件数量优化

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

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

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

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方法。

    类图

    文字描述总显得有些乏力,画一下类结构图来帮助我们理解代码。 如何解析应用安装包 - 图4ApkChecker是检查启动类,负责创建出ApkJob,再由ApkJob创建出任务列表,通过Factory工厂模式创建出实际的任务对象,最后返回任务结果,大致就是这样,所以大致的流程图应该是这样。

    细看源码

    ApkChecker部分源码:

    1. public final class ApkChecker {
    2. public static void main(String... args) {
    3. if (args.length > 0) {
    4. //创建出ApkChecker对象
    5. ApkChecker m = new ApkChecker();
    6. m.run(args);
    7. } else {
    8. System.out.println(INTRODUCT + HELP);
    9. System.exit(0);
    10. }
    11. }
    12. private void run(String[] args) {
    13. // 创建Job对象,调用run函数
    14. ApkJob job = new ApkJob(args);
    15. try {
    16. job.run();
    17. } catch (Exception e) {
    18. e.printStackTrace();
    19. System.exit(-1);
    20. }
    21. }
    22. }

    接着往下看ApkJob的run 方法: ```java public void run() throws Exception {

    1. //解析参数,创建对应的task
    2. if (parseParams()) {
    3. //创建解压任务对apk进行解压操作
    4. ApkTask unzipTask = TaskFactory.factory(TaskFactory.TASK_TYPE_UNZIP, jobConfig, new HashMap<String, String>());
    5. // 将任务放入preTasks集合中
    6. preTasks.add(unzipTask);
    7. //通过jobConfig获取 输出格式配置,
    8. for (String format : jobConfig.getOutputFormatList()) {
    9. //获取对应format的对象JobJsonResult或JobHtmlResult
    10. JobResult result = JobResultFactory.factory(format, jobConfig);
    11. if (result != null) {
    12. jobResults.add(result);
    13. } else {
    14. Log.w(TAG, "Unknown output format name '%s' !", format);
    15. }
    16. }
    17. //开始执行
    18. execute();
    19. } else {
    20. ApkChecker.printHelp();
    21. }

    } // 该函数作用就是根据命令行传递的参数,创建对应的Task,最后将其放入taskList集合中 private boolean parseParams() {

    1. if (args != null && args.length >= 2) {
    2. int paramLen = parseGlobalParams();
    3. for (int i = paramLen; i < args.length; i++) {
    4. if (args[i].startsWith("-") && !args[i].startsWith("--")) {
    5. Map<String, String> params = new HashMap<>();
    6. paramLen = parseParams(i + 1, args, params);
    7. if (!params.containsKey(JobConstants.PARAM_R_TXT)) {
    8. String inputDir = jobConfig.getInputDir();
    9. if (!Util.isNullOrNil(inputDir)) {
    10. params.put(JobConstants.PARAM_R_TXT, inputDir + "/" + ApkConstants.DEFAULT_RTXT_FILENAME);
    11. }
    12. }
    13. ApkTask task = createTask(args[i], params);
    14. if (task != null) {
    15. taskList.add(task);
    16. }
    17. i += paramLen;
    18. }
    19. }
    20. } else {
    21. return false;
    22. }
    23. return true;

    } // 最后看执行的过程 private void execute() throws Exception {

    1. try {
    2. //首先将 preTasks 准备中的任务 优先执行,然后将执行的结果
    3. for (ApkTask preTask : preTasks) {
    4. //任务初始化
    5. preTask.init();
    6. //任务同步执行,并获取结果
    7. TaskResult taskResult = preTask.call();
    8. if (taskResult != null) {
    9. TaskResult formatResult = null;
    10. //遍历预先配置好的结果集
    11. for (JobResult jobResult : jobResults) {
    12. //根据taskResult 与 jobResult 配置匹配结果
    13. formatResult = TaskResultFactory.transferTaskResult(taskResult.taskType, taskResult, jobResult.getFormat(), jobConfig);
    14. if (formatResult != null) {
    15. //将解压的任务结果放到jobResult中
    16. jobResult.addTaskResult(formatResult);
    17. }
    18. }
    19. }
    20. }
    21. //初始化其他任务
    22. for (ApkTask task : taskList) {
    23. task.init();
    24. }
    25. //解压缩还是同步执行,这里就用到了异步,创建一个线程池,通过代码发现,默认只有一个线程。
    26. List<Future<TaskResult>> futures = executor.invokeAll(taskList, timeoutSeconds, TimeUnit.SECONDS);
    27. for (Future<TaskResult> future : futures) {
    28. //通过get同步获取返回结果,由于线程池只有一个线程,所以看似并发,其实这里并没有。
    29. TaskResult taskResult = future.get();
    30. if (taskResult != null) {
    31. TaskResult formatResult = null;
    32. for (JobResult jobResult : jobResults) {
    33. //同样匹配结果最终放入到jobResult中
    34. formatResult = TaskResultFactory.transferTaskResult(taskResult.taskType, taskResult, jobResult.getFormat(), jobConfig);
    35. if (formatResult != null) {
    36. jobResult.addTaskResult(formatResult);
    37. }
    38. }
    39. }
    40. }
    41. //关闭线程池
    42. executor.shutdownNow();
    43. for (JobResult jobResult : jobResults) {
    44. //输出到文件中
    45. jobResult.output();
    46. }
    47. Log.d(TAG, "parse apk end, try to delete tmp un zip files");
    48. //删除解压的apk文件目录
    49. FileUtils.deleteDirectory(new File(jobConfig.getUnzipPath()));
    50. } catch (Exception e) {
    51. Log.e(TAG, "Task executor execute with error:" + e.getMessage());
    52. throw e;
    53. }

    }

  1. 整个过程,其实很简单,并不复杂,整个流程走完了,我们缺不知,导致任务执行了什么样子的代码,才能获取到相应的信息呢?我们来看几个具体的Task任务
  2. <a name="7qFJM"></a>
  3. #### MethodCountTask
  4. 为什么选择方法计数呢?因为我们经常会遇到项目方法数量统计需求,到底是如何统计的呢?我们是不是可以借助这次的学习就可以搞定了?随我来。直接上源码
  5. ```java
  6. //首先看下初始化方法,都做了哪些事情。
  7. @Override
  8. public void init() throws TaskInitException {
  9. super.init();
  10. // 获取解压后的apk文件目录
  11. String inputPath = config.getUnzipPath();
  12. if (Util.isNullOrNil(inputPath)) {
  13. throw new TaskInitException(TAG + "---APK-UNZIP-PATH can not be null!");
  14. }
  15. Log.i(TAG, "input path:%s", inputPath);
  16. // 根据路创建File对象,检查文件的属性,看是否匹配规则
  17. inputFile = new File(inputPath);
  18. if (!inputFile.exists()) {
  19. throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not exist!");
  20. } else if (!inputFile.isDirectory()) {
  21. throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not directory!");
  22. }
  23. // 如果匹配规则,就找到文件夹下的所有文件
  24. File[] files = inputFile.listFiles();
  25. try {
  26. if (files != null) {
  27. for (File file : files) {
  28. //找到dex结尾的文件
  29. if (file.isFile() && file.getName().endsWith(ApkConstants.DEX_FILE_SUFFIX)) {
  30. // 加入到dexFileNameList列表缓存中
  31. dexFileNameList.add(file.getName());
  32. //RandomAccessFile是Java中输入,输出流体系中功能最丰富的文件内容访问类
  33. //它提供很多方法来操作文件,包括读写支持
  34. //与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序可以直接跳到任意地方来读写数据。
  35. RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
  36. //随机文件对象加入到dexFileList中
  37. dexFileList.add(randomAccessFile);
  38. }
  39. }
  40. }
  41. } catch (FileNotFoundException e) {
  42. throw new TaskInitException(e.getMessage(), e);
  43. }
  44. //获取配置信息,目的是为了分组,是按类 class 或者按包 package
  45. if (params.containsKey(JobConstants.PARAM_GROUP)) {
  46. if (JobConstants.GROUP_PACKAGE.equals(params.get(JobConstants.PARAM_GROUP))) {
  47. group = JobConstants.GROUP_PACKAGE;
  48. } else if (JobConstants.GROUP_CLASS.equals(params.get(JobConstants.PARAM_GROUP))) {
  49. group = JobConstants.GROUP_CLASS;
  50. } else {
  51. Log.e(TAG, "GROUP-BY '" + params.get(JobConstants.PARAM_GROUP) + "' is not correct!");
  52. }
  53. }
  54. }

初始化完成后,就直接到了任务的执行,继续往下看

  1. //方法有些长,我们一行一行往下看
  2. @Override
  3. public TaskResult call() throws TaskExecuteException {
  4. try {
  5. //根据任务类型,结果类型,获取对应的 TaskResult对象,最终是TaskJsonResult或者TaskHtmlResult
  6. //这里默认
  7. TaskResult taskResult = TaskResultFactory.factory(getType(), TASK_RESULT_TYPE_JSON, config);
  8. if (taskResult == null) {
  9. return null;
  10. }
  11. //记录开始时间
  12. long startTime = System.currentTimeMillis();
  13. JsonArray jsonArray = new JsonArray();
  14. //这里就开始循环初始化时加入的dex文件列表,也就是RandomAccessFile随机访问对象
  15. for (int i = 0; i < dexFileList.size(); i++) {
  16. RandomAccessFile dexFile = dexFileList.get(i);
  17. //详见下面函数分析
  18. countDex(dexFile);
  19. //关闭文件流,防止泄漏
  20. dexFile.close();
  21. //计算内部方法数
  22. int totalInternalMethods = sumOfValue(classInternalMethod);
  23. //计算外部方法数
  24. int totalExternalMethods = sumOfValue(classExternalMethod);
  25. JsonObject jsonObject = new JsonObject();
  26. jsonObject.addProperty("dex-file", dexFileNameList.get(i));
  27. //按配置进行分组
  28. if (JobConstants.GROUP_CLASS.equals(group)) {
  29. List<String> sortList = sortKeyByValue(classInternalMethod);
  30. JsonArray classes = new JsonArray();
  31. for (String className : sortList) {
  32. JsonObject classObj = new JsonObject();
  33. classObj.addProperty("name", className);
  34. classObj.addProperty("methods", classInternalMethod.get(className));
  35. classes.add(classObj);
  36. }
  37. jsonObject.add("internal-classes", classes);
  38. } else if (JobConstants.GROUP_PACKAGE.equals(group)) {
  39. String packageName;
  40. for (Map.Entry<String, Integer> entry : classInternalMethod.entrySet()) {
  41. packageName = ApkUtil.getPackageName(entry.getKey());
  42. if (!Util.isNullOrNil(packageName)) {
  43. if (!pkgInternalRefMethod.containsKey(packageName)) {
  44. pkgInternalRefMethod.put(packageName, entry.getValue());
  45. } else {
  46. pkgInternalRefMethod.put(packageName, pkgInternalRefMethod.get(packageName) + entry.getValue());
  47. }
  48. }
  49. }
  50. List<String> sortList = sortKeyByValue(pkgInternalRefMethod);
  51. JsonArray packages = new JsonArray();
  52. for (String pkgName : sortList) {
  53. JsonObject pkgObj = new JsonObject();
  54. pkgObj.addProperty("name", pkgName);
  55. pkgObj.addProperty("methods", pkgInternalRefMethod.get(pkgName));
  56. packages.add(pkgObj);
  57. }
  58. jsonObject.add("internal-packages", packages);
  59. }
  60. jsonObject.addProperty("total-internal-classes", classInternalMethod.size());
  61. jsonObject.addProperty("total-internal-methods", totalInternalMethods);
  62. if (JobConstants.GROUP_CLASS.equals(group)) {
  63. List<String> sortList = sortKeyByValue(classExternalMethod);
  64. JsonArray classes = new JsonArray();
  65. for (String className : sortList) {
  66. JsonObject classObj = new JsonObject();
  67. classObj.addProperty("name", className);
  68. classObj.addProperty("methods", classExternalMethod.get(className));
  69. classes.add(classObj);
  70. }
  71. jsonObject.add("external-classes", classes);
  72. } else if (JobConstants.GROUP_PACKAGE.equals(group)) {
  73. String packageName = "";
  74. for (Map.Entry<String, Integer> entry : classExternalMethod.entrySet()) {
  75. packageName = ApkUtil.getPackageName(entry.getKey());
  76. if (!Util.isNullOrNil(packageName)) {
  77. if (!pkgExternalMethod.containsKey(packageName)) {
  78. pkgExternalMethod.put(packageName, entry.getValue());
  79. } else {
  80. pkgExternalMethod.put(packageName, pkgExternalMethod.get(packageName) + entry.getValue());
  81. }
  82. }
  83. }
  84. List<String> sortList = sortKeyByValue(pkgExternalMethod);
  85. JsonArray packages = new JsonArray();
  86. for (String pkgName : sortList) {
  87. JsonObject pkgObj = new JsonObject();
  88. pkgObj.addProperty("name", pkgName);
  89. pkgObj.addProperty("methods", pkgExternalMethod.get(pkgName));
  90. packages.add(pkgObj);
  91. }
  92. jsonObject.add("external-packages", packages);
  93. }
  94. jsonObject.addProperty("total-external-classes", classExternalMethod.size());
  95. jsonObject.addProperty("total-external-methods", totalExternalMethods);
  96. jsonArray.add(jsonObject);
  97. }
  98. ((TaskJsonResult) taskResult).add("dex-files", jsonArray);
  99. taskResult.setStartTime(startTime);
  100. taskResult.setEndTime(System.currentTimeMillis());
  101. //返回结果
  102. return taskResult;
  103. } catch (Exception e) {
  104. throw new TaskExecuteException(e.getMessage(), e);
  105. }
  106. }

countDex(dexFile) 函数分析

  1. private void countDex(RandomAccessFile dexFile) throws IOException {
  2. //按类分组的,内部方法Map,清除掉缓存
  3. classInternalMethod.clear();
  4. //按类分组的,外部依赖方法Map,清除掉缓存
  5. classExternalMethod.clear();
  6. //按包分组的,同上
  7. pkgInternalRefMethod.clear();
  8. pkgExternalMethod.clear();
  9. //使用的com.android.dexdeps 包下的DexData类,
  10. DexData dexData = new DexData(dexFile);
  11. dexData.load();
  12. //获取所有方法索引对象,包括内部方法和外部索引的方法
  13. MethodRef[] methodRefs = dexData.getMethodRefs();
  14. //获取所有外部类的索引数据
  15. ClassRef[] externalClassRefs = dexData.getExternalReferences();
  16. //获取混淆过的类
  17. Map<String, String> proguardClassMap = config.getProguardClassMap();
  18. String className = null;
  19. for (ClassRef classRef : externalClassRefs) {
  20. //获取类名
  21. className = ApkUtil.getNormalClassName(classRef.getName());
  22. if (proguardClassMap.containsKey(className)) {
  23. //匹配并赋值为混淆前的类名
  24. className = proguardClassMap.get(className);
  25. }
  26. if (className.indexOf('.') == -1) {
  27. continue;
  28. }
  29. //将类名放入到外部方法Map中,供下面匹配外部方法时使用
  30. classExternalMethod.put(className, 0);
  31. }
  32. //遍历所有方法,找到外部和内部方法,并分别加入到classExternalMethod、classInternalMethod Map中
  33. for (MethodRef methodRef : methodRefs) {
  34. //获取该方法的类名
  35. className = ApkUtil.getNormalClassName(methodRef.getDeclClassName());
  36. //匹配混淆前的类名
  37. if (proguardClassMap.containsKey(className)) {
  38. className = proguardClassMap.get(className);
  39. }
  40. if (!Util.isNullOrNil(className)) {
  41. if (className.indexOf('.') == -1) {
  42. continue;
  43. }
  44. //根据类名和外部方法存储的类信息匹配,匹配上就说明是外部方法引用。
  45. if (classExternalMethod.containsKey(className)) {
  46. classExternalMethod.put(className, classExternalMethod.get(className) + 1);
  47. } else if (classInternalMethod.containsKey(className)) {
  48. classInternalMethod.put(className, classInternalMethod.get(className) + 1);
  49. } else {
  50. classInternalMethod.put(className, 1);
  51. }
  52. }
  53. }
  54. //删除没有方法引用的类
  55. Iterator<String> iterator = classExternalMethod.keySet().iterator();
  56. while (iterator.hasNext()) {
  57. if (classExternalMethod.get(iterator.next()) == 0) {
  58. iterator.remove();
  59. }
  60. }
  61. }

通过源码的分析,其实原理就是使用DexData对象加载dexFile,最后getMethodRefs、getExternalReferences方法获取相关信息,最后通过外部的classExternalMethod 将所有的方法分类成内部方法和外部方法集合。

CountClassTask

经过上面的分析经验,我们再分析CountClassTask就简单了许多,直接找到核心代码如下:

  1. DexData dexData = new DexData(dexFile);
  2. dexData.load();
  3. dexFile.close();
  4. ClassRef[] defClassRefs = dexData.getInternalReferences();
  5. Set<String> classNameSet = new HashSet<>();
  6. for (ClassRef classRef : defClassRefs) {
  7. String className = ApkUtil.getNormalClassName(classRef.getName());
  8. if (classProguardMap.containsKey(className)) {
  9. className = classProguardMap.get(className);
  10. }
  11. if (className.indexOf('.') == -1) {
  12. continue;
  13. }
  14. classNameSet.add(className);
  15. }

上面使用了getExternalReferences,而这里直接使用getInternalReferences,获取所有内部类的索引数据,最终加入到一个HashSet中。整个过程简单明了。难道所有的Task的实现都是用到DexData对象处理的吗?并不是,我们来看下一个

UnusedAssetsTask

看类名,简单解释为未被使用的资源任务,其实就是找到assets文件夹下没有被依赖的资源,知道该任务的目的后,我们就来看看,它是如何实现的,先看下init中都做了什么准备

  1. //初始化方法
  2. @Override
  3. public void init() throws TaskInitException {
  4. super.init();
  5. //先拿到解压后的apk文件路径
  6. String inputPath = config.getUnzipPath();
  7. if (Util.isNullOrNil(inputPath)) {
  8. throw new TaskInitException(TAG + "---APK-UNZIP-PATH can not be null!");
  9. }
  10. inputFile = new File(inputPath);
  11. //同样的检测文件是否存在和检查文件的属性是否是文件夹
  12. if (!inputFile.exists()) {
  13. throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not exist!");
  14. } else if (!inputFile.isDirectory()) {
  15. throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not directory!");
  16. }
  17. //根据配置文件中忽略资源列表,放入到ignoreSet中,为了忽略一些文件的检查,比如确定资源是有用的,就不需要被检查,缩小范围。
  18. if (params.containsKey(JobConstants.PARAM_IGNORE_ASSETS_LIST) && !Util.isNullOrNil(params.get(JobConstants.PARAM_IGNORE_ASSETS_LIST))) {
  19. String[] ignoreAssets = params.get(JobConstants.PARAM_IGNORE_ASSETS_LIST).split(",");
  20. Log.i(TAG, "ignore assets %d", ignoreAssets.length);
  21. for (String ignore : ignoreAssets) {
  22. ignoreSet.add(Util.globToRegexp(ignore));
  23. }
  24. }
  25. File[] files = inputFile.listFiles();
  26. if (files != null) {
  27. for (File file : files) {
  28. if (file.isFile() && file.getName().endsWith(ApkConstants.DEX_FILE_SUFFIX)) {
  29. //同样的将dex文件,筛选出来,放入到dexFileNameList中
  30. dexFileNameList.add(file.getName());
  31. }
  32. }
  33. }
  34. }

初始化中,执行了常规的文件检查,params参数的整理,再将dex文件缓存到一个list中,待处理,再来看下call函数,看它如何找到了未被依赖资源。

  1. @Override
  2. public TaskResult call() throws TaskExecuteException {
  3. try {
  4. TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
  5. long startTime = System.currentTimeMillis();
  6. //创建assets目录的文件对象
  7. File assetDir = new File(inputFile, ApkConstants.ASSETS_DIR_NAME);
  8. //这里递归找到所有的文件,存储到assetsPathSet中,比较简单就不贴代码
  9. findAssetsFile(assetDir);
  10. //将忽略的文件,从assetsPathSet中剔除
  11. generateAssetsSet(assetDir.getAbsolutePath());
  12. Log.i(TAG, "find all assets count: %d", assetsPathSet.size());
  13. //核心的实现,解析代码中的assets引用,看下面贴的代码
  14. decodeCode();
  15. Log.i(TAG, "find reference assets count: %d", assetRefSet.size());
  16. assetsPathSet.removeAll(assetRefSet);
  17. JsonArray jsonArray = new JsonArray();
  18. for (String name : assetsPathSet) {
  19. jsonArray.add(name);
  20. }
  21. ((TaskJsonResult) taskResult).add("unused-assets", jsonArray);
  22. taskResult.setStartTime(startTime);
  23. taskResult.setEndTime(System.currentTimeMillis());
  24. return taskResult;
  25. } catch (Exception e) {
  26. throw new TaskExecuteException(e.getMessage(), e);
  27. }
  28. }
  29. //解析代码中的assets引用
  30. private void decodeCode() throws IOException {
  31. for (String dexFileName : dexFileNameList) {
  32. //这里用到之前 apktool-lib-2.4.0.jar 包中的类,根据dex文件获取DexBackedDexFile对象
  33. DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(inputFile, dexFileName), Opcodes.forApi(15));
  34. BaksmaliOptions options = new BaksmaliOptions();
  35. //apktool 的api,拿到排序好的类引用集合
  36. List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());
  37. for (ClassDef classDef : classDefs) {
  38. // 按空格将类里面的所有代码组合成一个数组
  39. String[] lines = ApkUtil.disassembleClass(classDef, options);
  40. if (lines != null) {
  41. //匹配资源文件的引用
  42. readSmaliLines(lines);
  43. }
  44. }
  45. }
  46. }
  47. private void readSmaliLines(String[] lines) {
  48. if (lines == null) {
  49. return;
  50. }
  51. for (String line : lines) {
  52. line = line.trim();
  53. //找到常量字符
  54. if (!Util.isNullOrNil(line) && line.startsWith("const-string")) {
  55. String[] columns = line.split(",");
  56. if (columns.length == 2) {
  57. //拿到常量字符中的资源名字
  58. String assetFileName = columns[1].trim();
  59. assetFileName = assetFileName.substring(1, assetFileName.length() - 1);
  60. if (!Util.isNullOrNil(assetFileName)) {
  61. for (String path : assetsPathSet) {
  62. //循环匹配,匹配上后就加入到assetRefSet中
  63. if (assetFileName.endsWith(path)) {
  64. assetRefSet.add(path);
  65. }
  66. }
  67. }
  68. }
  69. }
  70. }
  71. }

代码分析完,我们已经知道,原理其实就是对类里面的常量字符与assets目录中文件名做了匹配处理,能匹配上说明有被引用到,如果匹配失败,那就是没有被引用。分析到这里,你是不是对其他的Task也有些好奇且有了分析的方法了呢?由于篇幅原因我们就不再详细展开,有问题欢迎评论区提问。

小结

本期就先到这里,后面再补充详细的实战示例,毕竟实践才能出真理,由于时间的原因,后面还有8篇,当然我们肯定会在后续持续更新的,敬请谅解。