Android Dex分包原理

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

分包导致的问题

  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等待加载完成。

你可能感兴趣的:(AndroidApk编译原理)