Multidex分包Class依赖处理原理.md

之前在介绍 dx 生成 mulitdex 工作原理 的时候,在 processAllFiles 处理所有 class 的时候会对 - -main-dex-list=<file> 设置的对应参数 this.args.mainDexListFile 进行判断。

1
2
3
// 获取- -main-dex-list=<file> 的 keep 文件,虽然我们可能不会手动传,但是 sdk build tools 会自动生成 main dex 的 keep 文件。这个问题以后再聊,总之会走这里的分支。
if(this.args.mainDexListFile != null) {
FileNameFilter mainPassFilter = this.args.strictNameCheck?new Main.MainDexListFilter():new Main.BestEffortMainDexListFilter();

现在我们聊一下这个 keep 文件的来历。

当然它的作用就是在 dx 将 class 拆分成多个 dex 文件的时候,保证 classes.dex 中必要的类 被分出去,避免应用在启动时会造成 classNotFoundException 。关键在于如何生成这个文件。

当我们使用 mulitdex 处理 apk 时候,在工程 app —> build —> intermidiates —> muliti-dex —> 下会有这么两个文件:

  • maindexlist.txt : 以 class 全路径形式列出了 keep 在 main dex 中的所有 class;
  • manifest_keep.txt : 读取 appManifest 中的 application class,keep 住 application class 实例化过程中的调用类。

而 Google 在 SDK Build tools 下有这样几个文件

  1. mainDexClasses : 这个脚本太长了,总结一下它的作用

    1. 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      ...
      # 省略前面寻找和判断文件路径的过程。
      disableKeepAnnotated=
      # 根据 usage 猜测传入参数为
      # ./mainDexClasses --output /Users/handsomeyang/Desktop/outDir --disable-annotation-resolution-workaround --aapt-rules /Users/handsomeyang/Desktop/mainDexClassesNoAapt.rules <inputFiles>
      while true; do
      # 当 $1 = --output 为 trued , 可以 man expr 看一下 : 用法
      if expr "x$1" : 'x--output' >/dev/null; then
      # 将标准输出重定向到 output ,1 代表标准输出
      exec 1>$2
      # 删除前两位参数,即 删掉 --output /Users/handsomeyang/Desktop/outDir
      shift 2
      # 现在 $1 就是 --disable-annotation-resolution-workaround
      elif expr "x$1" : 'x--disable-annotation-resolution-workaround' >/dev/null; then
      # --disable-annotation-resolution-workaround 是禁用一种容易导致 bug 的 keep 方式
      disableKeepAnnotated=$1
      # 在把 --disable-annotation-resolution-workaround 删掉
      shift 1
      elif expr "x$1" : "x--aapt-rules" >/dev/null; then
      # ./mainDexClasses --aapt-rules /Users/handsomeyang/Desktop/mainDexClassesNoAapt.rules
      extrarules=$2
      # 把剩下参数也删掉
      shift 2
      # 此时脚本参数状态: ./mainDexClasses inputFiles
      else
      break
      fi
      done
      # 把 optinos 删掉后,如果没有 input files 显示 usage
      if [ $# -ne 1 ]; then
      echo "Usage : $0 [--output <output file>] <application path>" 1>&2
      exit 2
      fi
      tmpOut=`makeTempJar`
      trap cleanTmp 0
      # "\" 表示命令换行,
      # -injars {class_path} 指定要处理的应用程序jar,war,ear和目录
      # -libraryjars {classpath} 指定要处理的应用程序jar,war,ear和目录所需要的程序库文件
      # -dontoptimize 不优化输入的类文件
      # -dontobfuscate 不混淆输入的类文件
      # -outjars {class_path} 指定处理完后要输出的jar,war,ear和目录的名称
      # -include {filename} 从给定的文件中读取配置参数
      # 1>/dev/null 表示不再控制台输出命令标准输出
      # 把 <inputFiles> 通过 maDexClasses.rules 和 mainDexClassesNoAapt.rules 规则混淆
      # 并且压缩资源,输出到 temOut 的 jar 文件中
      "${proguard}" -injars ${@} -dontwarn -forceprocessing -outjars "${tmpOut}" \
      -libraryjars "${shrinkedAndroidJar}" -dontoptimize -dontobfuscate -dontpreverify \
      -include "${baserules}" -include "${extrarules}" 1>/dev/null || exit 10
      # java -cp dx.jar com.android.multidex.MainDexListBuilder --disable-annotation-resolution-workaround tmpOut.jar <inputFiles>
      # 把 混淆后的 jar 和 <inputFiles> 作为参数 调用 com.android.multidex.MainDexListBuilder 的 main 方法。
      java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} || exit 11
    2. 找到 mainDexClass.rulesdx.jar 路径

    3. 找到 proguard.shshrinkedAndroid.jar 路径

    4. 生成 mainDexClass-<pid>.tmp.jar

    5. 根据 mainDexClasses.rulesmainDexClassesNoAapt.rules 的 keep 规则进行混淆和资源压缩

    6. 调用 dx.jar 中的com.android.multidex.MainDexListBuilder main 方法来生成 maindexlist.txt

  2. mainDexClasses.rules

    1. 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      -keep public class * extends android.app.Instrumentation {
      <init>();
      }
      -keep public class * extends android.app.Application {
      <init>();
      void attachBaseContext(android.content.Context);
      }
      -keep public class * extends android.app.backup.BackupAgent {
      <init>();
      }
      # We need to keep all annotation classes because proguard does not trace annotation attribute
      # it just filter the annotation attributes according to annotation classes it already kept.
      -keep public class * extends java.lang.annotation.Annotation {
      *;
      }
      # Keep old fashion tests in the main dex or they'll be silently ignored by InstrumentationTestRunner
      -keep public class * extends android.test.InstrumentationTestCase {
      <init>();
      }
    2. 主要 keep 了 application 相关类。

  3. mainDexClassesNoAapt.rules

    1. 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      -keep public class * extends android.app.Activity {
      <init>();
      }
      -keep public class * extends android.app.Service {
      <init>();
      }
      -keep public class * extends android.content.ContentProvider {
      <init>();
      }
      -keep public class * extends android.content.BroadcastReceiver {
      <init>();
      }
    2. keep 了四大组件,即 组件实例化所需要的相关类。

既然要计算 main dex 的 class 引用情况,那么混淆和资源压缩,首先去除没有的引用减少计算量很好理解。

那么来看下 com.android.multidex.MainDexListBuilder 这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* This is a command line tool used by mainDexClasses script to build a main dex classes list. First
* argument of the command line is an archive, each class file contained in this archive is used to
* identify a class that can be used during secondary dex installation, those class files
* are not opened by this tool only their names matter. Other arguments must be zip files or
* directories, they constitute in a classpath in with the classes named by the first argument
* will be searched. Each searched class must be found. On each of this classes are searched for
* their dependencies to other classes. The tool also browses for classes annotated by runtime
* visible annotations and adds them to the list/ Finally the tools prints on standard output a list
* of class files names suitable as content of the file argument --main-dex-list of dx.
*
* 用于生成 main dex class list。
* 第一个参数是个包含所有 class 的 jar,就是混淆压缩后的 class 的 jar。
* 第二个参数就是混淆前的 class 。
* 工具还会读取被 RetentionPolicy.RUNTIME 和 RetentionPolicy.CLASS 注解的类添加到 maindexlist 中。
*/
/**
* args : --disable-annotation-resolution-workaround tmpOut.jar <inputFiles>
* 1st arg : --disable-annotation-resolution-workaround
* 2nd arg : tmpOut.jar
* 3rd arg : <inputFiles>
*/
public static void main(String[] args) {
int argIndex = 0;
boolean keepAnnotated;
for(keepAnnotated = true; argIndex < args.length - 2; ++argIndex) {
// 判断第一个参数是否 禁用 容易产生 bug 的生成 keep file 模式。
if (args[argIndex].equals("--disable-annotation-resolution-workaround")) {
keepAnnotated = false;
} else {
System.err.println("Invalid option " + args[argIndex]);
// Usage 就是不要手动调用这里的 main 方法,是给 mainDexClasses 脚本来调用的。
printUsage();
System.exit(1);
}
}
if (args.length - argIndex != 2) {
printUsage();
System.exit(1);
}
try {
/**
* @param keepAnnotated : true 禁用默生成 keep file 模式
* @param args[argIndex] : 混淆后的 jar file
* @param args[argIndex + 1] : mainDexClasses <input files> 多个 inputFile 用 ":" 隔开
*/
MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex], args[argIndex + 1]);
// toKeep set 就是 maindexlist
Set<String> toKeep = builder.getMainDexList();
printList(toKeep);
} catch (IOException var5) {
System.err.println("A fatal error occured: " + var5.getMessage());
System.exit(1);
}
}
public MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString){
ZipFile jarOfRoots = null;
Path path = null;
try {
try {
jarOfRoots = new ZipFile(rootJar);
} catch (IOException e) {
throw new IOException("\"" + rootJar + "\" can not be read as a zip archive. ("
+ e.getMessage() + ")", e);
}
// 把多个 input files 放进 list
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder = new ClassReferenceListBuilder(path);
// 1. 缓存所有 jar 报中的 class name
// 2. 把 寻找并添加 class 依赖
// 2.1 如果是类 : 递归寻找该类父类,添加到集合
// 2.2 如果是Filed : 递归寻找 Field 父类,添加到集合
// 2.3 如果是方法 : 递归寻找 返回值和每个参数的父类,添加到集合
mainListBuilder.addRoots(jarOfRoots);
for (String className : mainListBuilder.getClassNames()) {
// fileToKeep 就是 maindexlist
filesToKeep.add(className + CLASS_EXTENSION);
}
if (keepAnnotated) {
// 遍历 path 中的每一个 element 无论是类、方法、字段、如果带有 RetentionPolicy.RUNTIME 和 RetentionPolicy.CLASS 注解就添加到 keep files
keepAnnotated(path);
}
}
...
}
}

理下生成 maindexlist 流程:

  1. mainDexClasses 脚本进行混淆 class file,调用 dx.jar 中 MainDexListBuilder Main 方法,将 mainDexClasses.rulesmainDexClassesNoAapt.rules 的 keep 规则作为参数传入;
  2. main 方法处理参数,实例 ClassReferenceListBuilder 调用 addRoots 添加依赖;
  3. 判断 class 是否带有 运行时可见 注解
  4. mainDexListBuilder.getMainDexList() 返回 main dex 中需要的 class list;