Android Dex分包原理

作者: 会撒娇的犀犀利 | 来源:发表于2018-07-10 17:53 被阅读178次

    为什么要分包?

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

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

    相关文章

      网友评论

      本文标题:Android Dex分包原理

      本文链接:https://www.haomeiwen.com/subject/vsjqpftx.html