### 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还是一个解决方法数越界非常好的方案,可以在实际项目中使用。
- 前言
- 第1章 Activity的生命周期和启动模式
- 1.1 Activity的生命周期全面分析
- 1.1.1 典型情况下的生命周期分析
- 1.1.2 异常情况下的生命周期分析
- 1.2 Activity的启动模式
- 1.2.1 Activity的LaunchMode
- 1.2.2 Activity的Flags
- 1.3 IntentFilter的匹配规则
- 第2章 IPC机制
- 2.1 Android IPC简介
- 2.2 Android中的多进程模式
- 2.2.1 开启多进程模式
- 2.2.2 多进程模式的运行机制
- 2.3 IPC基础概念介绍
- 2.3.1 Serializable接口
- 2.3.2 Parcelable接口
- 2.3.3 Binder
- 2.4 Android中的IPC方式
- 2.4.1 使用Bundle
- 2.4.2 使用文件共享
- 2.4.3 使用Messenger
- 2.4.4 使用AIDL
- 2.4.5 使用ContentProvider
- 2.4.6 使用Socket
- 2.5 Binder连接池
- 2.6 选用合适的IPC方式
- 第3章 View的事件体系
- 3.1 View基础知识
- 3.1.1 什么是View
- 3.1.2 View的位置参数
- 3.1.3 MotionEvent和TouchSlop
- 3.1.4 VelocityTracker、GestureDetector和Scroller
- 3.2 View的滑动
- 3.2.1 使用scrollTo/scrollBy
- 3.2.2 使用动画
- 3.2.3 改变布局参数
- 3.2.4 各种滑动方式的对比
- 3.3 弹性滑动
- 3.3.1 使用Scroller7
- 3.3.2 通过动画
- 3.3.3 使用延时策略
- 3.4 View的事件分发机制
- 3.4.1 点击事件的传递规则
- 3.4.2 事件分发的源码解析
- 3.5 View的滑动冲突
- 3.5.1 常见的滑动冲突场景
- 3.5.2 滑动冲突的处理规则
- 3.5.3 滑动冲突的解决方式
- 第4章 View的工作原理
- 4.1 初识ViewRoot和DecorView
- 4.2 理解MeasureSpec
- 4.2.1 MeasureSpec
- 4.2.2 MeasureSpec和LayoutParams的对应关系
- 4.3 View的工作流程
- 4.3.1 measure过程
- 4.3.2 layout过程
- 4.3.3 draw过程
- 4.4 自定义View
- 4.4.1 自定义View的分类
- 4.4.2 自定义View须知
- 4.4.3 自定义View示例
- 4.4.4 自定义View的思想
- 第5章 理解RemoteViews
- 5.1 RemoteViews的应用
- 5.1.1 RemoteViews在通知栏上的应用
- 5.1.2 RemoteViews在桌面小部件上的应用
- 5.1.3 PendingIntent概述
- 5.2 RemoteViews的内部机制
- 5.3 RemoteViews的意义
- 第6章 Android的Drawable
- 6.1 Drawable简介
- 6.2 Drawable的分类
- 6.2.1 BitmapDrawable2
- 6.2.2 ShapeDrawable
- 6.2.3 LayerDrawable
- 6.2.4 StateListDrawable
- 6.2.5 LevelListDrawable
- 6.2.6 TransitionDrawable
- 6.2.7 InsetDrawable
- 6.2.8 ScaleDrawable
- 6.2.9 ClipDrawable
- 6.3 自定义Drawable
- 第7章 Android动画深入分析
- 7.1 View动画
- 7.1.1 View动画的种类
- 7.1.2 自定义View动画
- 7.1.3 帧动画
- 7.2 View动画的特殊使用场景
- 7.2.1 LayoutAnimation
- 7.2.2 Activity的切换效果
- 7.3 属性动画
- 7.3.1 使用属性动画
- 7.3.2 理解插值器和估值器 /
- 7.3.3 属性动画的监听器
- 7.3.4 对任意属性做动画
- 7.3.5 属性动画的工作原理
- 7.4 使用动画的注意事项
- 第8章 理解Window和WindowManager
- 8.1 Window和WindowManager
- 8.2 Window的内部机制
- 8.2.1 Window的添加过程
- 8.2.2 Window的删除过程
- 8.2.3 Window的更新过程
- 8.3 Window的创建过程
- 8.3.1 Activity的Window创建过程
- 8.3.2 Dialog的Window创建过程
- 8.3.3 Toast的Window创建过程
- 第9章 四大组件的工作过程
- 9.1 四大组件的运行状态
- 9.2 Activity的工作过程
- 9.3 Service的工作过程
- 9.3.1 Service的启动过程
- 9.3.2 Service的绑定过程
- 9.4 BroadcastReceiver的工作过程
- 9.4.1 广播的注册过程
- 9.4.2 广播的发送和接收过程
- 9.5 ContentProvider的工作过程
- 第10章 Android的消息机制
- 10.1 Android的消息机制概述
- 10.2 Android的消息机制分析
- 10.2.1 ThreadLocal的工作原理
- 10.2.2 消息队列的工作原理
- 10.2.3 Looper的工作原理
- 10.2.4 Handler的工作原理
- 10.3 主线程的消息循环
- 第11章 Android的线程和线程池
- 11.1 主线程和子线程
- 11.2 Android中的线程形态
- 11.2.1 AsyncTask
- 11.2.2 AsyncTask的工作原理
- 11.2.3 HandlerThread
- 11.2.4 IntentService
- 11.3 Android中的线程池
- 11.3.1 ThreadPoolExecutor
- 11.3.2 线程池的分类
- 第12章 Bitmap的加载和Cache
- 12.1 Bitmap的高效加载
- 12.2 Android中的缓存策略
- 12.2.1 LruCache
- 12.2.2 DiskLruCache
- 12.2.3 ImageLoader的实现446
- 12.3 ImageLoader的使用
- 12.3.1 照片墙效果
- 12.3.2 优化列表的卡顿现象
- 第13章 综合技术
- 13.1 使用CrashHandler来获取应用的crash信息
- 13.2 使用multidex来解决方法数越界
- 13.3 Android的动态加载技术
- 13.4 反编译初步
- 13.4.1 使用dex2jar和jd-gui反编译apk
- 13.4.2 使用apktool对apk进行二次打包
- 第14章 JNI和NDK编程
- 14.1 JNI的开发流程
- 14.2 NDK的开发流程
- 14.3 JNI的数据类型和类型签名
- 14.4 JNI调用Java方法的流程
- 第15章 Android性能优化
- 15.1 Android的性能优化方法
- 15.1.1 布局优化
- 15.1.2 绘制优化
- 15.1.3 内存泄露优化
- 15.1.4 响应速度优化和ANR日志分析
- 15.1.5 ListView和Bitmap优化
- 15.1.6 线程优化
- 15.1.7 一些性能优化建议
- 15.2 内存泄露分析之MAT工具
- 15.3 提高程序的可维护性