💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 13.2 使用multidex来解决方法数越界 在Android中单个dex文件所能够包含的最大方法数为65536,这包含Android FrameWork、依赖的jar包以及应用本身的代码中的所有方法。65536是一个很大的数,一般来说一个简单应用的方法数的确很难达到65536,但是对于一些比较大型的应用来说,65536就很容易达到了。当应用的方法数达到65536后,编译器就无法完成编译工作并抛出类似下面的异常: UNEXPECTED TOP-LEVEL EXCEPTION: com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536 at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:502) at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger. java:283) at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:491) at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:168) at com.android.dx.merge.DexMerger.merge(DexMerger.java:189) at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main. java:454) at com.android.dx.command.dexer.Main.runMonoDex(Main.java:303) at com.android.dx.command.dexer.Main.run(Main.java:246) at com.android.dx.command.dexer.Main.main(Main.java:215) at com.android.dx.command.Main.main(Main.java:106) 另外一种情况有所不同,有时候方法数并没有达到65536,并且编译器也正常地完成了编译工作,但是应用在低版本手机安装时异常中止,异常信息如下: E/dalvikvm: Optimization failed E/installd: dexopt failed on '/data/dalvik-cache/data@app@com.ryg. multidextest-2.apk@classes.dex' res = 65280 为什么会出现这种情况呢?其实是这样的,dexopt是一个程序,应用在安装时,系统会通过dexopt来优化dex文件,在优化过程中dexopt采用一个固定大小的缓冲区来存储应用中所有方法的信息,这个缓冲区就是LinearAlloc。LinearAlloc缓冲区在新版本的Android系统中其大小是8MB或者16MB,但是在Android 2.2和2.3中却只有5MB,当待安装的apk中的方法数比较多时,尽管它还没有达到65536这个上限,但是它的存储空间仍然有可能超出5MB,这种情况下dexopt程序就会报错,从而导致安装失败,这种情况主要在2.x系列的手机上出现。 可以看到,不管是编译时方法数越界还是安装时的dexopt错误,它们都给开发过程带来了很大的困扰。从目前的Android版本的市场占有率来说,Android 3.0以下的手机仍然占据着不到10%的比率,目前主流的应用都不可能放弃Android 3.0以下的用户,对于这些应用来说,方法数越界就是一个必须要解决的问题了。 如何解决方法数越界的问题呢?我们首先想到的肯定是删除无用的代码和第三方库。没错,这的确是必须要做的工作,但是很多情况下即使删除了无用的代码,方法数仍然越界,这个时候该怎么办呢?针对这个问题,之前很多应用都会考虑采用插件化的机制来动态加载部分dex,通过将一个dex拆分成两个或多个dex,这就在一定程度上解决了方法数越界的问题。但是插件化是一套重量级的技术方案,并且其兼容性问题往往较多,从单纯解决方法数越界的角度来说,插件化并不是一个非常适合的方案,关于插件化的意义将在第13.3节中进行介绍。为了解决这个问题,Google在2014年提出了multidex的解决方案,通过multidex可以很好地解决方法数越界的问题,并且使用起来非常简单。 在Android 5.0以前使用multidex需要引入Google提供的android-support-multidex.jar这个jar包,这个jar包可以在Android SDK目录下的extras/android/support/multidex/library/libs下面找到。从Android 5.0开始,Android默认支持了multidex,它可以从apk中加载多个dex文件。Multidex方案主要是针对AndroidStudio和Gradle编译环境的,如果是Eclipse和ant那就复杂一些,而且由于AndroidStudio作为官方IDE其最终会完全替代Eclipse ADT,因此本节中也不再介绍Eclipse中配置multidex的细节了。 在AndroidStudio和Gradle编译环境中,如果要使用multidex,首先要使用Android SDK Build Tools 21.1及以上版本,接着修改工程中app目录下的build.gradle文件,在defaultConfig中添加multiDexEnabled true这个配置项,如下所示。关于如何使用AndroidStudio和Gradle请读者自行查看相关资料,这里不再介绍。 android { compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "com.ryg.multidextest" minSdkVersion 8 targetSdkVersion 22 versionCode 1 versionName "1.0" // enable multidex support multiDexEnabled true } ... } 接着还需要在dependencies中添加multidex的依赖:compile 'com.android.support:multidex:1.0.0',如下所示。 dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.1.1' compile 'com.android.support:multidex:1.0.0' } 最终配置完成的build.gradle文件如下所示,其中加粗的部分是专门为multidex所添加的配置项: apply plugin: 'com.android.application' android { compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "com.ryg.multidextest" minSdkVersion 8 targetSdkVersion 22 versionCode 1 versionName "1.0" // enable multidex support multiDexEnabled true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.1.1' compile 'com.android.support:multidex:1.0.0' } 经过了上面的过程,还需要做另一项工作,那就是在代码中加入支持multidex的功能,这个过程是比较简单的,有三种方案可以选。 第一种方案,在manifest文件中指定Application为MultiDexApplication,如下所示。 <application android:name="android.support.multidex.MultiDexApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > ... </application> 第二种方案,让应用的Application继承MultiDexApplication,比如: public class TestApplication extends MultiDexApplication { … } 第三种方案,如果不想让应用的Application继承MultiDexApplication,还可以选择重写Application的attachBaseContext方法,这个方法比Application的onCreate要先执行,如下所示。 public class TestApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } } 现在所有的工作都已经完成了,可以发现应用不但可以编译通过了并且还可以在Android 2.x手机上面正常安装了。可以发现,multidex使用起来还是很简单的,对于一个使用multidex方案的应用,采用了上面的配置项,如果这个应用的方法数没有越界,那么Gradle并不会生成多个dex文件,如果方法数越界后,Gradle就会在apk中打包2个或多个dex文件,具体会打包多少个dex文件要看当前项目的代码规模。图13-2展示了采用multidex方案的apk中多个dex的分布情形。 :-: ![](https://img.kancloud.cn/a7/9e/a79eb250cb9c67a028c96fe2a6d6c85a_1349x573.png) 图13-2 普通apk和采用multidex方案的apk 上面介绍的是multidex默认的配置,还可以通过build.gradle文件中一些其他配置项来定制dex文件的生成过程。在有些情况下,可能需要指定主dex文件中所要包含的类,这个时候就可以通过--main-dex-list选项来实现这个功能。下面是修改后的build.gradle文件,在里面添加了afterEvaluate区域,在afterEvaluate区域内部采用了--main-dex-list选项来指定主dex中要包含的类,如下所示。 apply plugin: 'com.android.application' android { compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "com.ryg.multidextest" minSdkVersion 8 targetSdkVersion 22 versionCode 1 versionName "1.0" // enable multidex support multiDexEnabled true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } afterEvaluate { println "afterEvaluate" tasks.matching { it.name.startsWith('dex') }.each { dx -> def listFile = project.rootDir.absolutePath + '/app/maindexlist.txt' println "root dir:" + project.rootDir.absolutePath println "dex task found: " + dx.name if (dx.additionalParameters == null) { dx.additionalParameters = [] } dx.additionalParameters += '--multi-dex' dx.additionalParameters += '--main-dex-list=' + listFile dx.additionalParameters += '--minimal-main-dex' } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.1.1' compile 'com.android.support:multidex:1.0.0' } 在上面的配置文件中,--multi-dex表示当方法数越界时则生成多个dex文件,--main-dex-list指定了要在主dex中打包的类的列表,--minimal-main-dex表明只有--main-dex-list所指定的类才能打包到主dex中。它的输入是一个文件,在上面的配置中,它的输入是工程中app目录下的maindexlist.txt这个文件,在maindexlist.txt中则指定了一系列的类,所有在maindexlist.txt中的类都会被打包到主dex中。注意maindexlist.txt这个文件名是可以修改的,但是它的内容必须要遵守一定的格式,下面是一个示例,这种格式是固定的。 com/ryg/multidextest/TestApplication.class com/ryg/multidextest/MainActivity.class // multidex android/support/multidex/MultiDex.class android/support/multidex/MultiDexApplication.class android/support/multidex/MultiDexExtractor.class android/support/multidex/MultiDexExtractor$1.class android/support/multidex/MultiDex$V4.class android/support/multidex/MultiDex$V14.class android/support/multidex/MultiDex$V19.class android/support/multidex/ZipUtil.class android/support/multidex/ZipUtil$CentralDirectory.class 程序编译后可以反编译apk中生成的主dex文件,可以发现主dex文件的确只有maindexlist.txt文件中所声明的类,读者可以自行尝试。maindexlist.txt这个文件很多时候都是可以通过脚本来自动生成内容的,这个脚本需要根据当前的项目自行实现,如果不采用脚本,人工编辑maindexlist.txt也是可以的。 需要注意的是,multidex的jar包中的9个类必须也要打包到主dex中,否则程序运行时会抛出异常,告知无法找到multidex相关的类。这是因为Application对象被创建以后会在attachBaseContext方法中通过MultiDex.install(this)来加载其他dex文件,这个时候如果MultiDex相关的类不在主dex中,很显然这些类是无法被加载的,那么程序执行就会出错。同时由于Application的成员和代码块会先于attachBaseContext方法而初始化,而这个时候其他dex文件还没有被加载,因此不能在Application的成员以及代码块中访问其他dex中的类,否则程序也会因为无法加载对应的类而中止执行。在下面的代码中,模拟了这种场景,在Application的成员中使用了其他dex文件中的类View1。 public class TestApplication extends Application { private View1 view1 = new View1(); @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } } 上面的代码会导致如下运行错误,因此在实际开发中要避免这个错误。 E/AndroidRuntime: FATAL EXCEPTION: main Process: com.ryg.multidextest, PID: 12709 java.lang.NoClassDefFoundError: com.ryg.multidextest.ui.View1 at com.ryg.multidextest.TestApplication.<init>(TestApplication. java:14) at java.lang.Class.newInstanceImpl(Native Method) at java.lang.Class.newInstance(Class.java:1208) at android.app.Instrumentation.newApplication(Instrumentation. java:990) at android.app.Instrumentation.newApplication(Instrumentation. java:975) at android.app.LoadedApk.makeApplication(LoadedApk.java:504) ... Multidex方法虽然很好地解决了方法数越界这个问题,但它也是有一些局限性的,下面是采用multidex可能带来的问题: (1)应用启动速度会降低。由于应用启动时会加载额外的dex文件,这将导致应用的启动速度降低,甚至可能出现ANR现象,尤其是其他dex文件较大的时候,因此要避免生成较大的dex文件。 (2)由于Dalvik linearAlloc的bug,这可能导致使用multidex的应用无法在Android 4.0以前的手机上运行,因此需要做大量的兼容性测试。同时由于Dalvik linearAlloc的bug,有可能出现应用在运行中由于采用了multidex方案从而产生大量的内存消耗的情况,这会导致应用崩溃。 在实际的项目中,(1)中的现象是客观存在的,但是(2)中的现象目前极少遇到,综合来说,multidex还是一个解决方法数越界非常好的方案,可以在实际项目中使用。