Kaede Akatsuki

中二病也要开发 Android

给 Android App 启用 MultiDex 功能

App启动MultiDex主要是为了解决“65535方法数超标”以及“INSTALL_FAILED_DEXOPT”问题,就目前来说,对于使用Android Studio的朋友来说,MultiDex这个术语应该不陌生。而对于那些从早期使用Eclipse开发Android的人来说,这个词语则更加再熟悉不过了,因为用Eclipse开启MultiDex功能实在是太坑爹了(默默给Eclipse加一把土)。

出现的问题

65535问题

当App的功能越来越丰富、使用的库越来越多时,其包含的Java方法总数也越来越多,这时候就会出现65535问题。
在构建apk的时候限制了一个dex文件能包含的方法数,其总数不能超过65535(则64K,1K = 2^10 = 1024 , 64 * 1024 = 65535)。MultiDex, 顾名思义,是指多dex实现,大多数App,解压其apk后,一般只有一个classes.dex文件,采用MultiDex的App解压后可以看到有classes.dex,classes2.dex,… classes(N).dex,这样每个dex都可以最大承载64k个方法,很大限度地缓解了单dex方法数限制。
(2016-10-1 具体原因分析可以参考由Android-65K方法数限制引发的思考

LinearAlloc问题

现在这个问题已经不常见了,它多发生在2.x版本的设备上,安装时会提示INSTALL_FAILED_DEXOPT。这个问题发生在安装期间,在使用Dalvik虚拟机的设备上安装APK时,会通过DexOpt工具将Dex文件优化为odex文件,即Optimized Dex,这样可以提高执行效率(不同的设备需要不同的odex格式,所以这个过程只能安装apk后进行)。
LinearAlloc是一个固定大小的缓冲区,dexopt使用LinearAlloc来存储应用的方法信息,在Android的不同版本中有4M/5M/8M/16M等不同大小,目前主流4.x系统上都已到8MB或16MB,但是在Gingerbread或以下系统(2.2和2.3)LinearAlloc分配空间只有5M大小的。当应用的方法信息过多导致超出缓冲区大小时,会造成dexopt崩溃,造成INSTALL_FAILED_DEXOPT错误。

启用MultiDex解决问题

直接忽视Eclipse,在Android Studio中,可以通过以下方式来启用MultiDex。

环境要求

  • 使用 Android Studio 开发工具;
  • Android SDK Build Tools >= 21.1;
  • 使用 Support Repository 中的 multidex (刚出来时是0.1版本,目前已有有1.0.0和1.0.1);

配置build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"

defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...

multiDexEnabled true// Enable MultiDex.
}
...
}

dependencies {
compile 'com.android.support:multidex:1.0.1'
}

在代码里启动MultiDex

最后,在Java代码里启动MultiDex,有两种方式可以搞定。
方式一,使用MultiDexApplication。

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xxx">
<application
...
android:name="android.support.multidex.MultiDexApplication">
...
</application>
</manifest>

方式二,在自己的Application#attachBaseContext(Context)方法里添加以下代码。

1
2
3
4
5
6
public class MyApplication extends Application {
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);// Enable MultiDex.
}
}

存在的问题

MultiDex并不是万全的方案,Google貌似不太热衷于旧版本的兼容工作,通过阅读MultiDex Support库的源码,我们也能发现其代码写得貌似没有那么严谨。
目前来说,使用MultiDex可能存在以下问题。

NoClassDefFoundError

如果你在调用MultiDex#install(Context)做了别的工作,而这些工作需要用到的类却存在于别的dex文件里面(Secondary Dexes),就会出现类找不到的运行时异常。
正确的做法是把这些需要用到的类标记在multidex.keep清单文件里面,再在build.gradle里面启用该清单文件。

1
2
3
4
5
6
7
8
9
10
11
android {

defaultConfig {
multiDexEnabled true
multiDexKeepProguard file('multidex.pro')
}
}

dependencies {
compile'com.android.support:multidex:1.0.1'
}

multiDexKeepProguard使用的是类似于混淆文件的过滤规则,出了这个配置项之外还有multiDexKeepFile,这个要求你在清单文件里把所有的类都罗列出来。

卡顿问题

通过阅读源码,我们可以看出,目前Android 5.0以上的设备已经自身支持了MultiDex功能,也就是说在安装apk的时候,系统已经会帮我们把apk里面的所有dex文件都做好Optimize处理,所以不需要我们在代码里启用MultiDex了。但是对于Android 5.0以下的设置,则依然要求我们启用MultiDex。而这些系统的设备在第一次运行App的时候,需要对所有的Secondary Dexes文件都进行一次解压以及Optimize处理(生成odex文件),这段时间会有明显的耗时(大概5000毫秒左右,dex越多越大越明显),所有会产生明显的卡顿现象。
不过好在,在Application#attachBaseContext(Context)中,UI线程的阻塞是不会引发ANR的,只不过这段长时间的卡顿(白屏)还是会影响用户体验,解决方案能想到的有两种。
第一种,在安装一个新的apk的时候,先在Worker线程里做好MultiDex的Optimize工作,安装apk并启动后,直接使用之前Optimize产生的odex文件,这样就可以避免第一次启动时候的Optimize工作。
第二种,启动App的时候,先显示一个简单的Splash闪屏界面,然后启动Worker线程执行MultiDex#install(Context)工作,就可以避免UI线程阻塞。不过要确保启动以及启动MultiDex#install(Context)所需要的类都在Main Dex里面。

多进程同步

参考Google的MultiDex的源码,可以发现Google并没有考虑多进程同步的问题,如果App是多进程的,并且刚好同时有两个进程出发了MultiDex#install(Context)工作,可以会有多进程冲突的风险。
如果你采用了上面解决卡顿问题时说到的第二种方法,你也应该考虑多进程的问题,因为这种方法并不是在进程一启动就在主线程里面去做MultiDex初始化了,可能存在同步问题的风险。

其他问题

早期在使用MultiDex的时候,Android Studio会出现apk编译后无法安装的问题,然而使用命令行编译出来的apk文件又可以安装了,这个问题现在基本没有出现了,不再追究原因了。
参考资料: