MultiDex 编译过程

3,632 阅读7分钟

该文章已经收集到面试题整理(可在公众号点击底部 Tab 看到)。

在这之前的一篇文章中,我们模拟了关于65536问题的面试场景 关于 65536 限制与 MultiDex 会在面试中被问到的问题可能都在这了

然后归纳出了面试中可能会出现的问题:

  • Android 上为啥会有65536的限制,解释下原因。

  • Android 官方是如何解决65536问题的?MultiDex 在编译阶段和 app 运行阶段分别做了什么?

  • 使用 MultiDex 可能会造成什么问题?

  • 使用 MultiDex 后首次启动 app 有什么优化方向吗?

  • 如何将指定的 class 打进 mainDex ?

上篇已经写了Android 上为啥会有65536的限制,解释下原因。

今天开始讲第二个问题,Android 官方是如何解决65536问题的?

地球人都知道,MultiDex。

如何使用呢?地球人也都知道,需要在 gradle 中将 multiDexEnable 设为 true,将 application 继承 MultiDexApplication 或者在自己 application#attachBaseContext 调用 MultiDex.install。

那么本篇文章讲解释下 multiDexEnable 设为 true,也就是在编译阶段,MultiDex 方案究竟干了啥?

分析 MultiDexTransform

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

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

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

shrinkWithProguard

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

其中 manifest_keep.txt 里的内容:

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

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

=> 1

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

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

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

=> 2

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

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

=> 3

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

=> 4

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

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

返回的是 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

来看源码:

先看看 callDx :

再看 createMainDexList

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

将参数按顺序又实例化了一个 MainDexListBuilder,然后通过这个对象调用 getMainDexList() 取出 MainDexList,最后再做输出,那么看看 MainDexListBuilder :

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

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

可以看到 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

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

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

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

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

  • 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 可以看下:

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

一共分成五个部分:

=> 1

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

=> 2

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

=> 3

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

=> 4

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

=> 5

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

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

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

以上就是 MultiDex 在编译阶段的过程,当然在面试中回答这么详细一般不可能,除非面试官直接把代码给你,你带他一起过源码。

所以面试时回答这个问题大概围绕这两个点就行了:

1、执行完 MultiDexTransform 后会生成了一个在 mainDex 中出现的 classes 列表

2、执行 DexTransform 是将 mainDex 和其他 dex 的生成落实

当然,具体你围绕这两个点能回答多少就会因人而异了,比如第一点中生成的那四个文件分别是啥?mainDex 里的 classes 又是怎么确认出来的?再比如第二点中问一些关于 dx 工具的参数啥的,所以想做到能全面回答,那自己 debug 一遍这两个 task 就好了。

那么问题来了?该怎么 debug 打包流程呢?

下一篇文章来告诉你,这篇写的太长了,下篇弄个轻松的休息休息。