MultiDex 编译过程

Posted petewell

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MultiDex 编译过程相关的知识,希望对你有一定的参考价值。

分析 MultiDexTransform

当我们在 gradle 中将 multiDexEnabled 设为 true 后,编译 app 的过程中 Terminal 会多出一行: :app:transformClassesWithMultidexlistForDebug

显然 MultiDex 相关操作也是通过 Transform Api 完成了,自然我们查看 MultiDexTransform 源码,直接看 #transform 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void (@NonNull TransformInvocation invocation)
throws IOException, TransformException, InterruptedException {
...
try {
File input = verifyInputs(invocation.getReferencedInputs());
shrinkWithProguard(input);
computeList(input);
} catch (ParseException | ProcessException e) {
throw new TransformException(e);
}
}

哟吼,核心代码好少啊,一个 shrinkWithProguard, 一个 computeList

shrinkWithProguard

当我看到了方法名叫 shrinkWithProguard ,感觉很亲切啊,这不就是混淆器嘛,然后联想起 app 编译过程中输出的 app/build/intermediates/multi-dex/debug/ 下的那几个文件了:

技术图片

其中 manifest_keep.txt 里的内容:

1
2
# view androidManifest.xml #generated:15
-keep class android.support.multidex.MultiDexApplication { <init>(...); }

我的乖乖,shrinkWithProguard 方法势必和混淆器有扯不断的关系咯,来看看 shrinkWithProguard 具体的实现:

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
private void shrinkWithProguard(@NonNull File input) throws IOException, ParseException {
// => 1
dontobfuscate();
dontoptimize();
dontpreverify();
dontwarn();
dontnote();
forceprocessing();
// => 2
applyConfigurationFile(manifestKeepListProguardFile);
// => 3
if (userMainDexKeepProguard != null) {
applyConfigurationFile(userMainDexKeepProguard);
}
// => 4
// add a couple of rules that cannot be easily parsed from the manifest.
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>(); }");
keep("public class * extends java.lang.annotation.Annotation { *;}");
keep("class com.android.tools.fd.** {*;}"); // Instant run.
// => 5
// handle inputs
libraryJar(findShrinkedAndroidJar());
inJar(input);
// => 6
// outputs.
outJar(variantScope.getProguardComponentsJarFile());
printconfiguration(configFileOut);
// => 7
// run proguard
runProguard();
}

有点长,但是结构很清晰,我把上面代码块分为了7个部分:

=> 1

第一部分是干嘛的?我以第一个 dont 方法为例,dontobfuscate

1
2
3
public void dontobfuscate() {
configuration.obfuscate = false;
}

configuration 是 proguard 里的一个配置类,换言之,这样写的效果等同于我们在给 app 做混淆的时候在 proguard-rules.pro 写:

1
-dontobfuscate

好的,第一部分代码其实就是对混淆进行了配置。

=> 2

那接下来的第二部分就太好理解了,applyConfigurationFile(manifestKeepListProguardFile);

1
2
3
4
5
6
7
8
9
public void applyConfigurationFile(@NonNull File file) throws IOException, ParseException {
ConfigurationParser parser =
new ConfigurationParser(file, System.getProperties());
try {
parser.parse(configuration);
} finally {
parser.close();
}
}

manifestKeepListProguardFile 就是之前提到的 manifest_keep.txt,等于把 manifest_keep.txt 里的 keep 规则也加了进来。

=> 3

那第三部分和第二部分也是一样的咯,第三部分相当于是给开发人员的外部拓展入口,在 build.gradle 中配置:

1
multiDexKeepProguard file('./maindex-rules.pro')
=> 4

第四部分就是一大堆 keep 规则,包括 keep Application 、Annotation 啦。

以上四部分就是把 keep 规则搞好了,继续看第五步,比较重要,先看 findShrinkedAndroidJar

1
2
3
4
5
6
7
8
9
10
11
12
@NonNull
private File findShrinkedAndroidJar() {
...
File shrinkedAndroid = new File(
variantScope.getGlobalScope().getAndroidBuilder().getTargetInfo()
.getBuildTools()
.getLocation(),
"lib" + File.separatorChar + "shrinkedAndroid.jar");
...
return shrinkedAndroid;
}

返回的是 Android SDK 的 build-tools 里的 shrinkedAndroid.jar

=> 5

那很明显了,第五部分就是把 shrinkedAndroid.jar 和刚刚的 input 文件都加入 classpath 里。

=> 6

第六部分则是定义了一下相关输出文件。

=> 7

第七部分运行混淆器。

从以上流程我们能得知,shrinkWithProguard 就是将我们的原来编译好的 jar 文件在使用 proguard 后输出了一个满足规则的 jar ,这个 jar 在哪?下图里的 componentClasses.jar 就是了,并且 components.flags 就是 shrinkWithProguard 中前四步所生成的 keep 规则。

技术图片

computeList

来看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void computeList(File _allClassesJarFile) throws ProcessException, IOException {
Set<String> mainDexClasses = callDx(
_allClassesJarFile,
variantScope.getProguardComponentsJarFile());
...
if (userMainDexKeepFile != null) {
mainDexClasses = ImmutableSet.<String>builder()
.addAll(mainDexClasses)
.addAll(Files.readLines(userMainDexKeepFile, Charsets.UTF_8))
.build();
}
String fileContent = Joiner.on(System.getProperty("line.separator")).join(mainDexClasses);
Files.write(fileContent, mainDexListFile, Charsets.UTF_8);
}

先看看 callDx

1
2
3
4
5
6
7
private Set<String> callDx(File allClassesJarFile, File jarOfRoots) throws ProcessException {
EnumSet<AndroidBuilder.MainDexListOption> mainDexListOptions =
EnumSet.noneOf(AndroidBuilder.MainDexListOption.class);
...
return variantScope.getGlobalScope().getAndroidBuilder().createMainDexList(
allClassesJarFile, jarOfRoots, mainDexListOptions);
}

再看 createMainDexList

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
public Set<String> createMainDexList(
@NonNull File allClassesJarFile,
@NonNull File jarOfRoots,
@NonNull EnumSet<MainDexListOption> options) throws ProcessException {
...
String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
if (dx == null || !new File(dx).isFile()) {
throw new IllegalStateException("dx.jar is missing");
}
builder.setClasspath(dx);
builder.setMain("com.android.multidex.ClassReferenceListBuilder");
...
builder.addArgs(jarOfRoots.getAbsolutePath());
builder.addArgs(allClassesJarFile.getAbsolutePath());
CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();
mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)
.rethrowFailure()
.assertNormalExitValue();
LineCollector lineCollector = new LineCollector();
processOutputHandler.getProcessOutput().processStandardOutputLines(lineCollector);
return ImmutableSet.copyOf(lineCollector.getResult());
}

从上面的代码很明显能得知 createMainDexList 中调用了 com.android.multidex.ClassReferenceListBuilder 的 main 方法,然后将所得的 Set 进行返回,那么 ClassReferenceListBuilder 的 main 方法执行了啥?

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int argIndex = 0;
...
try {
MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex],
args[argIndex + 1]);
Set<String> toKeep = builder.getMainDexList();
printList(toKeep);
} catch (IOException e) {
...
}
}

将参数按顺序又实例化了一个 MainDexListBuilder,然后通过这个对象调用 getMainDexList() 取出 MainDexList,最后再做输出,那么看看 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
public MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString)
throws IOException {
ZipFile jarOfRoots = null;
Path path = null;
try {
try {
jarOfRoots = new ZipFile(rootJar);
} catch (IOException e) {
...
}
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder = new ClassReferenceListBuilder(path);
mainListBuilder.addRoots(jarOfRoots);
for (String className : mainListBuilder.getClassNames()) {
filesToKeep.add(className + CLASS_EXTENSION);
}
if (keepAnnotated) {
keepAnnotated(path);
}
} finally {
...
}
}
public Set<String> getMainDexList() {
return filesToKeep;
}

filesToKeep 变量最终的结果就是在 computeList 中的 mainDexClasses 的结果,那么在这个类里有两处地方调用了 filesToKeep.add,一处是 keepAnnotated 里,当存在运行时可见注解时会添加进来,另外一种就是遍历 mainListBuilder.getClassNames(),来看看这个又从哪来的?

首先用 allClassesJarFile 的 path 实例化 ClassReferenceListBuilder,然后将 jarOfRoots(这个 jar 文件就是我们执行 shrinkWithProguard 后生成的 componentClasss.jar) addRootsClassReferenceListBuilder 中,来看看 addRoots

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
public void addRoots(ZipFile jarOfRoots) throws IOException {
// keep roots
for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
entries.hasMoreElements();) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.endsWith(CLASS_EXTENSION)) {
...
classNames.add(name.substring(0, name.length() - CLASS_EXTENSION.length()));
}
}
// keep direct references of roots (+ direct references hierarchy)
for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
entries.hasMoreElements();) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.endsWith(CLASS_EXTENSION)) {
DirectClassFile classFile;
try {
classFile = path.getClass(name);
} catch (FileNotFoundException e) {
throw new IOException("Class " + name +
" is missing form original class path " + path, e);
}
addDependencies(classFile.getConstantPool());
}
}
}
Set<String> getClassNames() {
return classNames;
}

可以看到 classNames 变量是收集符合要求后的 classes 的集合,同时更应该看到这里的 keep 包括了两部分,一个是 jarOfRoots 文件的 root class,另一个是这个 root class 的直接引用,关于 keep 住 root class 的引用部分涉及到常量池,需要单开一篇文章做讲解,这里只要知道他 keep 住了这个 root class 的直接引用,以防运行这个 dex 时找不到类或方法。

到此,我们总算分析出了 callDx 干了啥,简单说就是通过 shrinkWithProguard 后生成的 componentClasss.jar 找出了所有应该在 mainDex 中出现的 class。

那么 callDx 下方还有段代码,很简单了,通过在 build.gradle 中配置需要加在 mainDex 的方法,如 multiDexKeepFile file(‘./main_dex_list.txt’)

最后会把所有在 mainDex 里的 class 输出在 maindexlist.txt 中:

技术图片

小结 MultiDexTransform

以上终于把 MultiDexTransform 讲完了,一句话总结,其实我们就是弄清楚了 mainDex 是如何得来的。那么这还不够啊,搞了半天才输出了一个 maindexlist.txt,所以继续搞起。

分析 DexTransform

1
2
:app:transformClassesWithMultidexlistForDebug
:app:transformClassesWithDexForDebug

在 app 编译过程中,在 MultiDex 后面后面执行的 Task 可谓是相当重要了,众所周知,将 class 文件转成 dex 文件就是这个 Task 做的了,那么先来看看 DexTrasnform 的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public DexTransform(
@NonNull DexOptions dexOptions,
boolean debugMode,
boolean multiDex,
@Nullable File mainDexListFile,
@NonNull File intermediateFolder,
@NonNull AndroidBuilder androidBuilder,
@NonNull Logger logger,
@NonNull InstantRunBuildContext instantRunBuildContext,
@NonNull Optional<FileCache> buildCache) {
this.dexOptions = dexOptions;
this.debugMode = debugMode;
this.multiDex = multiDex;
this.mainDexListFile = mainDexListFile;
this.intermediateFolder = intermediateFolder;
this.androidBuilder = androidBuilder;
this.logger = new LoggerWrapper(logger);
this.instantRunBuildContext = instantRunBuildContext;
this.buildCache = buildCache;
}

其中重要的变量大家肯定一眼就看出来了,一个是 multiDex 的 boolean,一个是 mainDexListFile 的 File,来看看是在哪里实例化的:

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
// TaskManager#createPostCompilationTasks
public void createPostCompilationTasks(
@NonNull TaskFactory tasks,
@NonNull final VariantScope variantScope) {
...
if (isMultiDexEnabled && isLegacyMultiDexMode) {
...
MultiDexTransform multiDexTransform = new MultiDexTransform(
variantScope,
extension.getDexOptions(),
null);
multiDexClassListTask =
transformManager.addTransform(tasks, variantScope, multiDexTransform);
multiDexClassListTask.ifPresent(variantScope::addColdSwapBuildTask);
} else {
multiDexClassListTask = Optional.empty();
}
...
DexTransform dexTransform = new DexTransform(
dexOptions,
config.getBuildType().isDebuggable(),
isMultiDexEnabled,
isMultiDexEnabled && isLegacyMultiDexMode ? variantScope.getMainDexListFile() : null,
variantScope.getPreDexOutputDir(),
variantScope.getGlobalScope().getAndroidBuilder(),
getLogger(),
variantScope.getInstantRunBuildContext(),
AndroidGradleOptions.getBuildCache(variantScope.getGlobalScope().getProject()));
Optional<AndroidTask<TransformTask>> dexTask =
transformManager.addTransform(tasks, variantScope, dexTransform);
...
}

可以看到,在 MultiDexTransform 实例化之后就去实例化了 DexTransform,实际上是将是否开启了 multidex 和 MultiDexTransform 生成的 maindexlist.txt 传给了 DexTransform,拿了参数做了啥?来看看 DexTransform 的 transform 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final AndroidBuilder androidBuilder;
public void (@NonNull TransformInvocation transformInvocation)
throws TransformException, IOException, InterruptedException {
...
androidBuilder.convertByteCode(
inputFiles,
outputDir,
multiDex,
mainDexListFile,
dexOptions,
outputHandler);
....
}

继续往,由于调用链比较深,需要重点关注的我再单独贴代码:

  • AndroidBuilder#convertByteCode =>
  • DexByteCodeConverter#convertByteCode =>
  • DexProcessBuilder#build =>
  • ProcessInfoBuilder#createJavaProcess =>
  • com.android.dx.command.Main#main =>
  • com.android.dx.command.dexer.Main#main =>
  • com.android.dx.command.dexer.Main#run,这个 run 可以看下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int run(Arguments arguments) throws IOException {
...
args = arguments;
...
try {
if (args.multiDex) {
return runMultiDex();
} else {
return runMonoDex();
}
} finally {
closeOutput(humanOutRaw);
}
}

这里判断了是否需要运行 MultiDex,如果需要则执行 com.android.dx.command.dexer.Main 的 runMultiDex 方法,这个 Main 类相当重要,也比较复杂,建议自行阅读,我只把 runMultiDex 方法执行的意义说一下:

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
// com.android.dx.command.dexer.Main#runMultiDex
private int runMultiDex() throws IOException {
...
// => 1
if (args.mainDexListFile != null) {
classesInMainDex = new HashSet<String>();
readPathsFromFile(args.mainDexListFile, classesInMainDex);
}
// => 2
dexOutPool = Executors.newFixedThreadPool(args.numThreads);
// => 3
if (!processAllFiles()) {
return 1;
}
...
if (outputDex != null) {
dexOutputFutures.add(dexOutPool.submit(new DexWriter(outputDex)));
outputDex = null;
}
try {
dexOutPool.shutdown();
...
// => 4
for (Future<byte[]> f : dexOutputFutures) {
dexOutputArrays.add(f.get());
}
} catch (Exception e) {
...
}
...
if (args.outName != null) {
File outDir = new File(args.outName);
assert outDir.isDirectory();
// => 5
for (int i = 0; i < dexOutputArrays.size(); i++) {
OutputStream out = new FileOutputStream(new File(outDir, getDexFileName(i)));
try {
out.write(dexOutputArrays.get(i));
} finally {
closeOutput(out);
}
}
}
return 0;
}

一共分成五个部分:

=> 1

MultiDexTransform 生成的 maindexlist.txt 里的内容转成 classesInMainDex Set 集合。

=> 2

创建线程池,默认大小为 4 ,之后 每个 dex 的生成都会在单独线程去执行。

=> 3

这一步是核心步骤,将所有 classes 打成 mainDex 和 其他 dex,待会再看。

=> 4

将每个线程生成的 dex 字节流加入 dexOutputArrays 集合中。

=> 5

依次输出 classes.dex、classes2.dex …

刚刚第三部分留着没讲,现在来看看:

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
private boolean processAllFiles() {
createDexFile();
...
try {
if (args.mainDexListFile != null) {
// with --main-dex-list
FileNameFilter mainPassFilter = args.strictNameCheck ? new MainDexListFilter() :
new BestEffortMainDexListFilter();
// forced in main dex
for (int i = 0; i < fileNames.length; i++) {
processOne(fileNames[i], mainPassFilter);
}
...
// remaining files
for (int i = 0; i < fileNames.length; i++) {
processOne(fileNames[i], new NotFilter(mainPassFilter));
}
}
...
} catch (StopProcessing ex) {
...
}
...
return true;
}

可以看到,先是强行将 maindexlist.txt 里的 class 打进 mainDex,再去处理其他的 dex,关于其他的 dex 是根据什么规则产生的,有兴趣的可以自行去研究。

原文:大专栏  MultiDex 编译过程


以上是关于MultiDex 编译过程的主要内容,如果未能解决你的问题,请参考以下文章

Android分包MultiDex原理详解

Flutter 的 Multidex 问题

当我们在谈论multidex65535时,我们在谈论什么

当我们在谈论multidex65535时,我们在谈论什么

如何安装android-support-multidex.jar

android MultiDex multidex原理下超出方法数的限制问题