文章预览
该文章已经收集到面试题整理(可在公众号点击底部 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 打包流程呢?
下一篇文章来告诉你,这篇写的太长了,下篇弄个轻松的休息休息。
………………………………