前言

随着应用越来越大,很多大型应用都会遇到方法数的爆棚以及方法信息存储区的问题,该篇文章主要以这两种问题为背景,介绍dex拆分、加载以及插件化方案的一些技术点。

65536 与 INSTALL_FAILED_DEXOPT

  1. 生成的apk在android 2.3或之前的机器上无法安装,提示INSTALL_FAILED_DEXOPT

  2. 方法数量过多,编译时出错,提示:

    1
    Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

Android 2.3 INSTALL_FAILED_DEXOPT 的问题

该问题由dexopt的LinearAlloc限制引起的, 在Gingerbread或者以下系统LinearAllocHdr分配空间只有5M大小的。

Dalvik linearAlloc是一个固定大小的缓冲区。在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。 目前Android 4.x提高到了8MB或16MB,在很大程度上解决了这个问题。

超过最大方法数限制的问题

该问题是由DEX文件格式限制。一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于DEX文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是Android打包的DEX过程中, 单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536;

解决方案

  • Android系统的LinearAlloc空间大小,在高版本上已经提升了,所以在一定程度上,不会出现这个问题,Facebook则采用了一种hack的方式,直接修改虚拟机的LinearAlloc空间大小:Dalvik patch for Facebook for Android
  • 拆分Dex,官方有提供方案Google Multidex,让一个apk里支持多个.dex文件,这样就可以突破65536的限制,但该方案有一定的弊端,下面会提到
  • 插件化,将整个工程的划分为Host+多个插件的形式,这样就可以将方法分散到各个插件中,按需加载各个模块。从软件工程的角度来看,这种方案会更好,动态部署,并行开发,更高的编译效率,但这种方案对已有的大项目来讲,重构起来影响非常大,实现成本也相对较高
  • 使用ProGuard等其它优化工具清除项目中无用方法,第三方库中的方法进行清除处理,这个可以作为优化的手段,但对于大型应用上,并不能从根本上解决方法数的问题

上面也提到,在高版本上,LinearAlloc方法区的空间已经扩大了很多,所以LinearAlloc的问题很少会再出现。采用dex的拆分以及插件化后并按需加载,对避免LinearAlloc的问题也更有益。

各个公司所采取的方案

  • 美团点评、手Q、微信采用dex拆分方案
  • 携程采用插件化方案
  • 手淘近期发布的Atlas,采用了容器化的概念,所有业务的bundle运行在一个容器中,按需加载,也可以看做是一种高级的插件化

下面主要介绍一下dex方案和插件化的主要方案流程和遇到的问题

Dex方案

如何拆分Dex

官方已经推出Google Multidex方,但官方的问题在于:如果DEX文件过大时,处理时间就会越长,很容易引发ANR。因此通常会采取动态加载dex的方式,并尽量保持首次加载的mainDex中的类尽量少。

mainDex:主要是一些Android的基础组件和类,比如ApplicationContentProviderServiceReceiver以及必要的Activity和其依赖集。如果mainDex还包括应用的首页等界面,那么也要将相关的依赖都放到mainDex中。

如何动态加载Dex

dex拆分完了,也解决了首次加载mainDex的问题,那么如何动态加载其它dex呢?比如,加入我们在mainDex中放入了首页,这时候,点击某一个入口,而该入口的业务放在了其它的Dex中,这时候如何加载呢?

通常的处理肯定都是先判断是否已经加载过,如果没加载就先展示一个友好的loading界面然后去加载。思路确实是这样,具体的实现因人而异。

微信、QQ空间的方案

在app进入首页之前,在异步线程中加载

加载逻辑这边主要判断是否已经dexopt,若已经dexopt,即放在attachBaseContext加载,反之放于地球中用线程加载。怎么判断?其实很低级,因为在微信中,若判断revision改变,即将dex以及dexopt目录清空。只需简单判断两个目录dex名称、数量是否与配置文件的一致。

美团点评的方案

按需加载

在Activity的启动过程,修改Instrumentation中的逻辑,因为Instrumentation有关Activity启动相关的方法大概有:execStartActivity、newActivity等等,在这些方法中添加代码逻辑进行判断这个Class是否加载了,如果加载则直接启动这个Activity,如果没有加载完成则启动一个等待的Activity显示给用户,然后在这个Activity中等待后台Secondary DEX加载完成,完成后自动跳转到用户实际要跳转的Activity。

插件化方案

携程采用了这种方案,个人觉得从开发的角度来看,该方案其实是一种比较干净的方案。每个业务团队开发独立的apk,互不干涉,较高的编译效率。因此,解决的不仅仅是方法数和LinearAlloc的问题,插件化的难点在于资源的编译和加载,最终也会涉及到multidex。下面就简单介绍一下这种方案的一些关键技术点。

我们知道一个Android应用主要以APK方式安装,每一个APK主要的部分为代码和资源,如何编译进去,以及如何加载它们是关键的两个问题,所以插件化主要会涉及到两个阶段的问题:

  • 编译期:资源和代码的编译
  • 运行时:资源和代码的加载

编译期

资源的编译

采用AAPT(资源编译依赖一个强大的命令行工具)对资源的编译流程进行改造。

  • 可以对一个已存在的apk包作为依赖资源参与编译,解决资源编译问题
  • 通过aapt可以给每个子apk中的资源分配不同头字节PackageID,解决多个项目的资源冲突问题

代码的编译

对Java代码的编译大家比较熟悉,只需要注意以下几个问题即可:

  • classpath

    Java源码编译中需要找齐所有依赖项,classpath就是用来指定去哪些目录、文件、jar包中寻找依赖。

  • 混淆

    安全需要,Android工程通常都会被混淆,混淆的原理和配置可参考Proguard手册。

运行期

资源加载

平时使用到的资源都是通过AssetManager类和Resources类来访问的。获取它们的方法位于Context类中,因此复写这两个方法即可达到访问指定资源的目的:

1
2
3
4
5
6
7
8
9
10
11
private final Resources mResources;
@Override
public AssetManager getAssets() {
return getResources().getAssets();
}
@Override
public Resources getResources() {
return mResources;
}

另外通过要需要接管所有Activity、Service等组件的创建(以便对Resources类进行替换)。具体可以通过篡改mInstrumentation为自己的InstrumentationHookmInstrumentation是负责创建Activity等组件的类,每次创建Activity的时候把它的mResources类替换为DelegateResources。更多关于Activity的创建过程可以搜索相关资料,需要对Activity的创建和启动进行定制的时候,都会涉及到该部分的知识。

代码加载

这部分其实就是上面的dex装载方案,就不多介绍了

插件化的好处

  • 各业务的代码和项目控制上做到了高内聚低耦合,极大降低了沟通成本,提高了工作效率
  • 解决了方法数的问题
  • 大大提高编译速度
  • 按需加载,启动速度优化,告别黑屏和启动ANR
  • 可以作为hotFix方案,不仅支持class,也支持资源fix

携程的插件化方案也已经开源:https://github.com/CtripMobile/DynamicAPK

其它的插件化方案也有多种,具体的可以参考Trinea的文章Android 插件化 动态升级

最后

简单的总结下,这里介绍了一些中大型应用会遇到的空间和LinearAlloc空间的问题,最难的是资源的处理和装载,需要利用android提供的一些编译工具对编译流程进行改造。绕不过的是multidex,也会涉及到Activity等组件的初始化流程,Activity的启动过程这块的知识也是比较重要。本篇文章只是对目前插件化和dex的实现方案做了简要的概述,更细节的部分还是建议看各个公司发布的相关文章。

参考

美团Android DEX自动拆包及动态加载简介

携程插件化和动态加载实践

Android拆分与加载Dex的多种方案对比

Android 插件化 动态升级