Android Dex分包原理
为什么要分包?
1、65536问题
-
导致因素
随着项目apk的庞大以及加入更多的第三方库,app的方法数已经超过了65536,会导致程序根本跑不起来。
-
原因
在生成.dex文件后由于有很多冗余的资源,所以Android中会对dex文件进行优化,Davlik模式下利用dexopt
工具进行优化,而dexopt有两个问题:-
Dexopt
会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个short 类型
来保存的,导致了方法 id 的数目不能够超过65536个, 当一个项目足够大的时候,显然这个方法数的上限是不够的; - Dexopt 使用
LinearAlloc
来存储应用的方法信息, Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃;
-
-
在
ART模式
下 ,采用的是dexoat
工具,对应生成art虚拟执行可执行的.oat
文件,这个是包含多个dex文件;
2、怎么解决这个问题
- 在gradle中添加MultiDex支持,加载classes2.dex
multiDexEnabled true
- 执行MultiDex.install()
@Override protected void attachBaseContext (Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
分包导致的问题
-
API14 之前的不能支持分包
Dalvik linearalloc bug -
在冷启动时因为需要安装dex文件,如果dex文件过大时,处理时间过长,很容易引发ANR(Application Not Responding);
-
采用MultiDex方案的应用因为需要申请一个很大的内存,在运行时可能导致程序的崩溃,这个主要是因为Dalvik linearAlloc 的一个限制,这个限制在 Android 4.0 (API level 14)已经增加了, 应用也有可能在低于 Android 5.0 (API level 21)版本的机器上触发这个限制;
-
分包后,不同依赖项目间的dex文件函数相互调用,
报错找不到方法
Android系统对分包的影响
-
Android 5.0以下:
运行在Davlik虚拟机上,优化使用dexopt工具
并分包,每次运行先加载主包,然后反射子包,存在主包子包的先后问题; -
Android 5.0以上:
运行在ART虚拟机上,优化使用dexoat
工具,生成多个包含dex文件的.oat文件
,.oat文件是混合了主包子包,已经在APK安装时生成,故程序运行起来不存在主包子包的加载先后问题;
MultiDex的基本原理
通过DexFile
来加载Secondary DEX,并存放在BaseDexClassLoader
的DexPathList
中。
解决分包导致调用找不到对应类
1、微信加载方案
首次加载在地球中页中, 并用线程去加载(但是 5.0 之前加载 dex 时还是会挂起主线程一段时间(不是全程都挂起))。
-
dex 形式
微信是将包放在assets
目录下的,在加载 Dex 的代码时,实际上传进去的是 zip,在加载前需要验证 MD5,确保所加载的 Dex 没有被篡改。 -
dex 类分包规则
分包规则即将所有 Application、ContentProvider 以及所有 export 的 Activity、Service 、Receiver 的间接依赖集都必须放在主 dex
。 -
加载 dex 的方式
加载逻辑这边主要判断是否已经 dexopt,若已经 dexopt,即放在 attachBaseContext 加载,反之放于地球中用线程加载。怎么判断?因为在微信中,若判断 revision 改变,即将 dex 以及 dexopt 目录清空。只需简单判断两个目录 dex 名称、数量是否与配置文件的一致。
总的来说,这种方案用户体验较好,缺点在于太过复杂,每次都需重新扫描依赖集,而且使用的是比较大的间接依赖集。
2、 Facebook 加载方案
Facebook的思路是将 MultiDex.install()
操作放在另外一个经常进行的。
-
dex 形式
与微信相同。 -
dex 类分包规则
Facebook 将加载 dex 的逻辑单独放于一个单独的 nodex 进程中。
<activity
android:exported="false"
android:process=":nodex"
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
所有的依赖集为 Application、NodexSplashActivity 的间接依赖集即可。
- 加载 dex 的方式
因为NodexSplashActivity
的 intent-filter 指定为Main
和LAUNCHER
,所以一打开 App 首先拉起 nodex 进程,然后打开NodexSplashActivity
进行MultiDex.install()
。如果已经进行了 dexpot 操作的话就直接跳转主界面,没有的话就等待 dexpot 操作完成再跳转主界面。
这种方式好处在于依赖集非常简单,同时首次加载 dex 时也不会卡死。但是它的缺点也很明显,即每次启动主进程时,都需先启动 nodex 进程。尽管 nodex 进程逻辑非常简单,这也需100ms以上。
3、美团加载方案
- dex 形式
在 gradle 生成 dex 文件的这步中,自定义一个 task 来干预 dex 的生产过程,从而产生多个 dex 。
tasks.whenTaskAdded { task ->
if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doLast {
makeDexFileAfterProguardJar();
}
task.doFirst {
delete "${project.buildDir}/intermediates/classes-proguard";
String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
generateMainIndexKeepList(flavor.toLowerCase());
}
} else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doFirst {
ensureMultiDexInApk();
}
}
}
-
dex 类分包规则
把 Service、Receiver、Provider 涉及到的代码都放到主 dex 中,而把 Activity 涉及到的代码进行了一定的拆分,把首页 Activity、Laucher Activity 、欢迎页的 Activity 、城市列表页 Activity 等所依赖的 class 放到了主 dex 中,把二级、三级页面的 Activity 以及业务频道的代码放到了第二个 dex 中,为了减少人工分析 class 的依赖所带了的不可维护性和高风险性,美团编写了一个能够自动分析 class 依赖的脚本, 从而能够保证�主 dex 包含 class 以及他们所依赖的所有 class 都在其内,这样这个脚本就会在打包之前自动分析出启动到主 dex 所涉及的所有代码,保证主 dex 运行正常。
-
加载 dex 的方式
通过分析 Activity 的启动过程,发现 Activity 是由 ActivityThread 通过 Instrumentation 来启动的,那么是否可以在 Instrumentation 中做一定的手脚呢?通过分析代码 ActivityThread 和 Instrumentation 发现,Instrumentation 有关 Activity 启动相关的方法大概有:execStartActivity、 newActivity 等等,这样就可以在这些方法中添加代码逻辑进行判断这个 class 是否加载了,如果加载则直接启动这个 Activity,如果没有加载完成则启动一个等待的 Activity 显示给用户,然后在这个 Activity 中等待后台第二个 dex 加载完成,完成后自动跳转到用户实际要跳转的 Activity;这样在代码充分解耦合,以及每个业务代码能够做到颗粒化的前提下,就做到第二个 dex 的按需加载了。
美团的这种方式对主 dex 的要求非常高,因为第二个 dex 是等到需要的时候再去加载。重写Instrumentation 的 execStartActivity 方法,hook 跳转 Activity 的总入口做判断,如果当前第二个 dex 还没有加载完成,就弹一个 loading Activity等待加载完成。
最后,希望此篇博客对大家有所帮助,欢迎提出问题及建议共同探讨,如有兴趣可以关注我的博客,谢谢!