MainDex 优化记

若是你对本文感兴趣,也许你对个人公众号也会有兴趣,可扫下方二维码或搜索公众微信号:mxszgg java

tips: 本文基于 AGP 3.0.1 源码分析android

MainDex 打入规则分析

“maindex method 超过 65536 了,咋被打爆了呢?”git

在过去很长一段时间内咱们的应用 maindex 会被打爆,因而大佬们使用了DexKnifePlugin 来解决问题,可是后来 AGP 上了 3.0.1 以及其余问题的出现,DexKnifePlugin 已经不是可以很良好地适用于咱们的 app 中了,因而巴神(公众号:巴巴巴掌)用了另外一个比较优雅的方案,经过 hook transform 来达到了咱们的目的,可是终究是一个 hook 方案而且它不可以运用于 D8 编译器,因而须要一个更加优雅的方案,咱们必须得从源码中了解到究竟什么样的类会打入 maindex——github

打开 MultiDexTransform/MainDexListTransform 源码(本文以 MultiDexTransform 为例),直接看向 transform() 源码 ——bash

直接看向 181 行,这里的 input 变量是全部的 class 文件集合,接下来进入 182 行 ——微信

214-227 行就是 maindex 的一部分 keep 规则,第一部分 manifestKeepListProguardFile 路径为 /app/build/intermediates/multi-dex/release/manifest_keep.txt,从 TaskManager#createProcessResTask() 方法中能够了解到当编译环境为 multidexEnabled 打开而且当前 minSdk 版本小于21的时候才会有这个文件,再从 AAPT 源码中可知 AAPT 将会扫描应用的 AndroidManifest.xml 而后将其中的 application、instrumentation、自己或其父 application 处于另外一个进程的四大组件 keep 住,keep 的内容将会是自己以及构造器方法,相似以下:闭包

-keep class com.joker.maindexkeep.App { <init>(...); }
复制代码

第二部分是 useMainDexKeepProguard,这个是开发者在 gradle 中配置的但愿可以被 keep 在主 dex 的文件,其配置规则与混淆配置文件相同,这里就不作额外扩展了;第三部分是写死的配置规则,有 instrumentation、application 等,须要注意的是 226 行,全部的注解类也将会被 keep 住;接下来就是设置 Proguard 的输入输出文件,最后就是 238 行执行 proguard 了,具体内部逻辑就不跟踪了,最后输出文件也就是 234 行所说起的路径为 /app/build/intermediates/multi-dex/release/componentClasses.jar,打开该 jar 包能够看到包中内容是彻底根据上述的全部 keep 规则所生成的 ——app

进行完第二步,就是第三步 computeList() 了——ide

该方法第一步是计算全部的 mainDexClasses;第二步是判断 userMainDexKeepFile 文件是否为空,该文件是由开发者在 gradle 配置文件中经过 multiDexKeepFile 配置的,配置规则就是直接填充 class 文件的全路径限定名;最后就是写入 mainDexListFile 中,该文件路径为 /app/build/intermediates/multi-dex/release/maindexlist.txt,该文件实际上就是全部会被打入 maindex 中的 class 文件集合。三步看下来只有第一步须要分析,callDx() 源码以下——源码分析

看向 280-288 行代码能够知道,若是开发者配置了 keepRuntimeAnnotatedClasses 的话,mainDexListOptions 将会添加一个 DISABLE_ANNOTATION_RESOLUTION_WORKAROUND 配置,接着看到290行并跟踪下去,createMainDexList() ——

这段代码看起来很复杂,实际上就是就是根据当前编译环境找到 sdk 中的 dx.jar(1199-1205行),而后调用 dx.jar 中 ClassReferenceListBuilder 类的 main 方法,第一个参数就是以前 callDx() 中所说起的参数(若是配置了 keepRuntimeAnnotatedClasses 的话),第二个参数是以前生成的 componentClasses.jar,第三个参数是一个 jar 包,该 jar 包是混淆 task 生成的,有且仅有应用全部的 class 文件。最后此方法返回了一个 Set,这个 Set 就是最终会打入主 dex 的全部的 class 的全限定路径名集合。

虽然说调用的是 dx.jar 中的 ClassReferenceListBuilder,实际上与 AGP 中自带的 ClassReferenceListBuilder 类无多大差别,因此不妨直接看 AGP 中的 ClassReferenceListBuilder 的 main 方法——

在这里须要告诉各位读者的是前面所提到的 createMainDexList() 所返回的集合实际上就是第 93 行代码的结果,也就是 MainDexListBuilder#getMainDexList() 的结果,因此看一下该方法返回的是一个什么 ——

实际上返回了一个 Set,那么全局不妨搜下该 Set 的 add 方法所调用的地方,实际上共有两处——

1.MainDexListBuilder#getClassNames() 方法的逻辑就不在此给各位读者解答了,直接给结论—— componentClasses.jar 中全部的类及其引用类的集合。

2.该方法的逻辑是若是当前类或类的方法或类的字段被运行时注解所修饰了的话,那么也将会被添加到 filesToKeep 变量中,可是 keepAnnotated() 的执行逻辑从上一张图中的 128 行代码能够看出,只有 keepAnnotated 变量为 true 的时候才会执行,那么何时该变量为 true 呢?从 MainDexListBuilder#main() 方法中能够知道,默认状况下 keepAnnotated() 就是会为 true 的,除非当开发者手动将 keepRuntimeAnnotatedClasses 设为 false。

综上两点所述和前面对 MultiDexTransform#computeList() 方法所述,最终打入 maindex 中的 class 会有如下几个部分组成:

  • 默认的 keep 规则中的类(如 application、annotation);
  • 开发者经过 multiDexKeepProguard 配置的类;
  • 前二者通过 proguard shrink 后的引用类以及引用类的常量池、字段、方法返回值所涉及到的类及其全部父类;
  • 全部类自己、类方法、类字段其中任一被运行时注解所修饰的类;(可选项)
  • 开发者经过 multiDexKeepFile 配置的类。

MainDex 瘦身

根据以上五点咱们不难总结出如下几个优化点:

1.注解类不要写成内部类:眼尖的小伙伴发现本文第三张配图中,实际上内部类 a 是注解类,可是外部类 a 并非注解类,可是因为内部类 a 是外部类 a 的内部类(emm..)因此实际上外部类 a 也会被 keep 住并被打入 componentClasses.jar 中,而 componentClasses.jar 中全部类的引用类将会被打入 maindex 中。这很可怕,举个例子,若是开发者在一个庞大的 activity 中写了一个注解内部类,那么该 activity 的引用类都将会被打入 maindex,那么可想而知 maindex 多么容易被打爆。

2.若是仅仅是想打一个类到 maindex 里面,那么请使用 multiDexKeepFile 配置文件进行配置,由于使用 multiDexKeepProguard 配置的配置类,不只是其自己,还有它的引用类也将会被打入 maindex。

3.注解类 RetentionPolicy 规范化:若是不是用于反射的注解,那么没有必要将它设为 RUNTIME 的,这样就能够减小第四点中所说起的类。

4.笔者在前面标记了第四点为可选项是由于实际上开发者能够经过在 app/build.gradle 中配置如下闭包,这样的话就不会进行第四项规则匹配——

android {
	dexOptions {
		keepRuntimeAnnotatedClasses false
	}
}
复制代码

当设置以上闭包后,maindex 将不会再扫描类自己、类方法或类字段被运行时注解所修饰的类,也并不会将它们打入 maindex 中,这是一个减少 maindex 体积的瘦身利器!

容易忽略的地方

前面总结了几点瘦身的建议,可是仍是有不少容易使人忽略的地方:

1.因为混淆执行在打 dex 以前,这意味着开发者试图想要 keep 的类名可能已经被混淆过了,因此在使用 multiDexKeepProguard/multiDexKeepFile 配置的时候,开发者须要先在 proguard-rules.pro 中配置该类相关信息。

2.前面一直谈论的是 MultiDexTransform 源码,笔者在文章前说过除了 MultiDexTransform 还能够是 MainDexListTransform ——

首先进行 callDx(),可是 292 行第一个参数为 false,这将不会对应用中全部的类进行注解扫描,紧接着 266-268 行是添加 multiDexKeepFile 所配置的文件,最后 270-277 是进行注解扫描,因此区别有如下两点:

  1. keepRuntimeAnnotatedClasses 规则也一样适用于 multiDexKeepFile 所配置的文件,而在 MultiDexTransform 中 keepRuntimeAnnotatedClasses 是不会适用于 multiDexKeepFile 所配置的文件,因此前面提到的第2点优化不适用于 MainDexListTransform。
  2. MainDexListTransform 是不会进行全局运行时注解扫描的,这对于一个应用中运行时注解处处分布的 app 来讲将会大大减小 Maindex 体积。

那么何时 gradle 编译的时候是如何选择 MultiDexTransform 与 MainDexListTransform 的呢?答案位于 TaskManager 类中 ——

若是开发者在 gradle.properties 文件中显式配置 android.useDexArchive=false(默认为true,无需配置)则将选择 MultiDexTransform,若是当前是 debug buildType 则选用 MainDexListTransform,最后就是取决于 android.enableD8 的值了。

其它优化

在实际项目中也许并非由笔者说的这么简单,一方面是因为历史代码遗留问题,不方便重构前人所写的不规范的注解类;另外一方面 java 或三方库提供的注解咱们没法修改,例如 javax 包中的注解都是 RUNTIME 的,由于服务端不会像客户端通常对性能要求更为严苛,而 Dagger2 引用的就是 javax 包中的注解,例如像 butterknife 10.0.0 版本中的注解类已被改为为 RUNTIME 等等等等;还有可能一律而论的忽略全部的使用 RUNTIME 注解的类可能会有必定的麻烦与风险。也许不少场景下并不可以简单使用 keepRuntimeAnnotatedClasses 来解决问题,针对这种问题笔者开源了 thinAnnotation,这个开源库能够在混淆以后,打 dex 以前将开发者配置的注解类删除,从而使得构造 maindex 的时候减小该注解类及使用该注解类的类的引入,更加具体的介绍欢迎各位读者去阅读 README 了(本文样例也放在了 thinAnnotation 中)。

相关文章
相关标签/搜索