Android Dex分包原理

Java基础

浏览数:22

2019-8-22

为什么要分包?

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);
}

分包导致的问题

  1. API14 之前的不能支持分包 Dalvik linearalloc bug

  2. 在冷启动时因为需要安装dex文件,如果dex文件过大时,处理时间过长,很容易引发ANR(Application Not Responding);

  3. 采用MultiDex方案的应用因为需要申请一个很大的内存,在运行时可能导致程序的崩溃,这个主要是因为Dalvik linearAlloc 的一个限制,这个限制在 Android 4.0 (API level 14)已经增加了, 应用也有可能在低于 Android 5.0 (API level 21)版本的机器上触发这个限制;

  4. 分包后,不同依赖项目间的dex文件函数相互调用,报错找不到方法

Android系统对分包的影响

  • Android 5.0以下:
    运行在Davlik虚拟机上,优化使用dexopt工具并分包,每次运行先加载主包,然后反射子包,存在主包子包的先后问题;

  • Android 5.0以上:
    运行在ART虚拟机上,优化使用dexoat工具,生成多个包含dex文件的.oat文件,.oat文件是混合了主包子包,已经在APK安装时生成,故程序运行起来不存在主包子包的加载先后问题;

MultiDex的基本原理

通过DexFile来加载Secondary DEX,并存放在BaseDexClassLoaderDexPathList中。

解决分包导致调用找不到对应类

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 指定为 MainLAUNCHER ,所以一打开 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等待加载完成。

最后,希望此篇博客对大家有所帮助,欢迎提出问题及建议共同探讨,如有兴趣可以关注我的博客,谢谢!

作者:会撒娇的犀犀利