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