MultiDex 编译过程 Posted 2020-11-16 petewell
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MultiDex 编译过程相关的知识,希望对你有一定的参考价值。
当我们在 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
android Manifest.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 {
dontobfuscate();
dontoptimize();
dontpreverify();
dontwarn();
dontnote();
forceprocessing();
applyConfigurationFile(manifestKeepListProguardFile);
if (userMainDexKeepProguard != null ) {
applyConfigurationFile(userMainDexKeepProguard);
}
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.** {*;}" );
libraryJar(findShrinkedAndroidJar());
inJar(input);
outJar(variantScope.getProguardComponentsJarFile());
printconfiguration(configFileOut);
runProguard();
}
有点长,但是结构很清晰,我把上面代码块分为了7个部分:
=> 1 第一部分是干嘛的?我以第一个 dont 方法为例,dontobfuscate :
1
2
3
public void dontobfuscate () {
configuration.obfuscate = false ;
}
configuration 是 proguard 里的一个配置类,换言之,这样写的效果等同于我们在给 app 做混淆的时候在 proguard-rules.pro 写:
好的,第一部分代码其实就是对混淆进行了配置。
=> 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) addRoots 到 ClassReferenceListBuilder 中,来看看 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 {
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()));
}
}
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 讲完了,一句话总结,其实我们就是弄清楚了 mainDex 是如何得来的。那么这还不够啊,搞了半天才输出了一个 maindexlist.txt ,所以继续搞起。
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
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
private int runMultiDex () throws IOException {
...
if (args.mainDexListFile != null ) {
classesInMainDex = new HashSet<String>();
readPathsFromFile(args.mainDexListFile, classesInMainDex);
}
dexOutPool = Executors.newFixedThreadPool(args.numThreads);
if (!processAllFiles()) {
return 1 ;
}
...
if (outputDex != null ) {
dexOutputFutures.add(dexOutPool.submit(new DexWriter(outputDex)));
outputDex = null ;
}
try {
dexOutPool.shutdown();
...
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();
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 ) {
FileNameFilter mainPassFilter = args.strictNameCheck ? new MainDexListFilter() :
new BestEffortMainDexListFilter();
for (int i = 0 ; i < fileNames.length; i++) {
processOne(fileNames[i], mainPassFilter);
}
...
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原理下超出方法数的限制问题