查看原文
其他

让你的方法数不要超过64K,MainDex优化记

张果果 郭霖 2019-05-22



今日科技快讯


近日,据BuzzFeed News报道,美国新闻聚合网站BuzzFeed News进行最新调查发现,百度旗下Android开发商DO Global的多款应用存在广告欺诈行为,并向用户隐瞒了应用程序的所有权信息之后,谷歌开始大规模下架这些应用。


作者简介


大家周日好,虽然有点加班的感觉,但是为了小长假忍一波,新的一周继续加油!

本篇文章来自 jokermonn 的投稿,分享了他的MainDex优化历程,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

jokermonn 的博客地址:

https://juejin.im/user/57201bb479bc440066b06ee4


MainDex打入规则分析


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

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

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

直接看向 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 规则所生成的 。

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

该方法第一步是计算所有的 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 配置的类;

  • 前两者所涉及到的类的引用类;

  • 所有类本身、类方法、类字段其中任一被运行时注解所修饰的类;(可选项)

  • 开发者通过 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 

注意到 290 行的注释以及 270-277 行的代码可知实际上没多大变化,只是前面提到的 keepRuntimeAnnotatedClasses 规则也同样适用于 multiDexKeepFile 所配置的文件,而在 MultiDexTransform 中 keepRuntimeAnnotatedClasses 是不会适用于 multiDexKeepFile 所配置的文件,所以前面提到的第2点优化不适用于 MainDexListTransform。那么什么时候 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 中)。

thinAnnotation:

https://github.com/jokermonn/thinAnnotation


推荐阅读:

面试必问的HashMap,你真的了解吗?

在Android Studio中编写一个自己的模板

巨佬Jake Wharton曾说过:一个App只需要一个Activity


欢迎关注我的公众号,学习技术或投稿

长按上图,识别图中二维码即可关注

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存