目录

  • 前言
  • 问题
  • 解决
  • minSdkVersion小于21的配置
  • minSdkVersion 大于等于21的配置
  • 其它的一些补充
  • 参考


前言

我们知道,java代码在执行时首先会由JDK编译成.class字节码文件,然后由JVM运行该程序时会读取字节码文件并同步将其翻译成Native code交由机器执行。

Android源代码到运行app的过程大致也是如此:先由一系列工具将源代码编译成dex字节码文件,构建apk,然后交由android运行时去解释执行。

在android5.0以前,android系统使用的是Dalvik运行时,它解释执行时采用的是JITJust-In-Time)方式,也就是app在启动时开始翻译字节码。Android5.0及以后,Android运行时换成了ART,它采用AOTAhead-Of-Time)方式翻译字节码,就是在安装apk时将dex文件提前翻译成Native code保存到.oat文件中,运行app时直接读取执行。

通常情况下,每个apk中只会编译成一个dex文件,其中包含了android框架方法,引用库的方法,以及自己写的方法。然而单个dex文件中最多包含65536个方法,简称64K限制

问题

随着项目规模的扩大,源程序中的方法数最终会超过64k限制,此时如果不进行处理则可能报如下错误:

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

或者

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

解决

  • 简化项目依赖,避免引入庞大的库。
  • 移除项目中无用的代码,使用ProGuard对代码进行压缩。
  • 启动dex分包配置,将方法分到多个dex包中。

这里我们说下dex分包的配置:

minSdkVersion小于21的配置

在Android5.0以前使用Dalvik运行时执行应用代码,默认情况下Dalvik运行时只能使用单个dex文件。要绕过此限制则需引入Dalvik可执行文件分包支持库。它会根据需要构建一个主classes.dex文件与多个辅助classesN.dex文件,主dex文件保障app能够正常启动,然后app启动后会使用特殊的类加载器在所有的dex文件中搜索方法。

配置方式:

  • 修改模块级别build.gradle
android {
    defaultConfig {
        multiDexEnabled true    //启用分包
    }
}

dependencies {
    //添加dex分包支持库
    compile 'com.android.support:multidex:1.0.3'
}
  • 配置Application:
  • 如果没有替换Application,需要编辑AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application
        android:name="android.support.multidex.MultiDexApplication" >
        ...
    </application>
</manifest>
  • 如果替换了Application,将替换的Application的基类改成MultiDexApplication
public class MyApplication extends MultiDexApplication { ... }
  • 若替换了Application但是又无法替换基类。则覆写attachBaseContext方法并调用MutilDex.install(this)
public class MyApplication extends  SomeOtherApplication {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

minSdkVersion 大于等于21的配置

Android5.0以后,系统采用ART运行时,原生支持加载多个dex文件,它会在应用安装时进行预编译,扫描所有的classesN.dex文件,并将其编译成单个.oat文件。

配置如下:

  • 只需在模块级的build.gradle文件中启用dex分包即可。
android {
    defaultConfig {
        ...
        minSdkVersion 21 //<<大于21
        multiDexEnabled true //启用分包
    }
}

其它的一些补充

  • 一般来说,超过64k限制往往是我们代码写的太冗余,直接依赖或者传递依赖太多……我们应该尽量规避64k限制而不是无脑启用dex分包:
  • 简化依赖,避免引入庞大的库
  • 通过ProGuard移除未使用代码
  • Dalvik可执行分包支持库存在一些局限:
  • 构建dex文件时会执行复杂决策判断类放置到哪个dex文件中,因而会增加项目构建的时间。
  • 启动app时安装dex文件的过程相当复杂,如果辅助dex文件太大则很可能会引起ANR。
  • Android4.0以前的设备可能无法运行。
  • Android5.0之前配置分包的应用如果发出庞大的内存申请,则可能在运行时崩溃。
  • 某些情况下主dex文件中可能未自动提供app启动时所需的类(通常是因为某些类的引用路径太过隐晦,构建时自动决策没有正确判断将其添加至主dex文件),这将会在启动时抛出java.lang.NoClassDefFoundError的错误,此时需要使用构建类型的multiDexKeepFilemultiDexKeepProguard属性来声明这些类,以手动将其添加至主dex文件,以保证app正常启动。
  • multiDexKeepFile
    首先创建一个文件multidex-config.txt用于保存需要添加的类:
com/example/MyClass.class
com/example/MyOtherClass.class
  • 然后在启用分包的模块build.gradle中声明multiDexKeepFile属性:
android {
    buildTypes {
        release {
            multiDexKeepFile file 'multidex-config.txt'
        }
    }
}
  • multiDexKeepProguard
    首先需要创建一个multiDexKeepProguard文件multidex-config.pro,语法与Proguard一样,向其中添加需要keep的类:
-keep class com.example.MyClass
-keep class com.example.MyClassToo
-keep class com.example.** { *; }
  • 然后在启用分包的模块build.gradle中声明multiDexKeepProguard属性:
android {
    buildTypes {
        release {
            multiDexKeepProguard 'multidex-config.pro'
        }
    }
}
  • ART中对分包构建的优化:
    minSdkVersion为21及以上时,会启用一个pre-dexing的构建功能,它将会为每个应用模块和每个依赖构建单独的dex文件,然后将这些dex文件直接加入apk,且不执行合并操作,因而可以节省相当的构建时间。
    如果项目最低支持版本不能低于21,则可以使用productFlavors创建开发定制构建变体(Build Variants)与发布构建变体,在不同的变体中指定不同的minSdkVersion
android {
    defaultConfig {
        ...
        multiDexEnabled true
    }
    productFlavors {
        dev {
            // Enable pre-dexing to produce an APK that can be tested on
            // Android 5.0+ without the time-consuming DEX build processes.
            minSdkVersion 21
        }
        prod {
            // The actual minSdkVersion for the production version.
            minSdkVersion 14
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    compile 'com.android.support:multidex:1.0.3'
}

这样,在开发测试时我们使用dev构建变体,利用ART的pre-dexing功能加快构建速度。发布正式版时,使用release构建变体,保留对低版本平台的支持。

  • 上面提到ART的pre-dexing功能可以加快构建速度,然而利用这个功能构建的app在Android5.x系统上运行时可能会崩溃,并且抛出类似以下错误:
java.lang.RuntimeException: Unable to instantiate application com.package.TestApplication: 
    java.lang.ClassNotFoundException: 
    Didn't find class "com.package.Application" on path: DexPathList[[zip file "/data/app/com.package.testapp-1/base.apk"],nativeLibraryDirectories=[/data/app/com.package.testapp-1/lib/x86_64, /vendor/lib64, /system/lib64]]

这是因为在android5.x系统的ART中,有段代码限制了读取dex分包的数量为100,而Application或者其它某些类刚好不在前100个dex分包中,导致运行时抛出了XXX未找到的异常。一个简单的解决办法是关闭默认启用的pre-dexing构建功能:

android {
    ...
    dexOptions {
        preDexLibraries = false
    }
}

这当然会使ART对分包构建的优化失效,所以更好的解决办法是简化项目的依赖,调整项目结构。

参考

https://developer.android.com/studio/build/multidex#limitationshttps://stackoverflow.com/questions/43666425/android-5-x-classnotfoundexception-works-fine-on-6-0/47890503#47890503?newreg=f4d0f4571e22466495e3ec69a026bfc9