Android 使用android-support-multidex解決Dex超出方法數的限制問題,讓你的應用不再爆棚

 

隨著應用不斷迭代,業務線的擴展,應用越來越大(比如集成瞭各種第三方sdk或者公共支持的jar包,項目耦合性高,重復作用的類越來越多),相信很多人都遇到過如下的錯誤:
 

UNEXPECTED TOP-LEVEL EXCEPTION:
java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501)
at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:282)
at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)
at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)
at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
at com.android.dx.command.dexer.Main.run(Main.java:230)
at com.android.dx.command.dexer.Main.main(Main.java:199)
at com.android.dx.command.Main.main(Main.java:103)

沒錯,你的應用中的Dex 文件方法數超過瞭最大值65536的上限,簡單來說,應用爆棚瞭.

那麼讓我們看一下為什麼會引起這種錯誤:

 

Android系統中,一個App的所有代碼都在一個Dex文件裡面。Dex是一個類似Jar的存儲瞭多有Java編譯字節碼的歸檔文件。因為Android系統使用Dalvik虛擬機,所以需要把使用Java Compiler編譯之後的class文件轉換成Dalvik能夠執行的class文件。這裡需要強調的是,DexJar一樣是一個歸檔文件,裡面仍然是Java代碼對應的字節碼文件。當Android系統啟動一個應用的時候,有一步是對Dex進行優化,這個過程有一個專門的工具來處理,叫DexOptDexOpt的執行過程是在第一次加載Dex文件的時候執行的。這個過程會生成一個ODEX文件,即Optimised Dex。執行ODex的效率會比直接執行Dex文件的效率要高很多。但是在早期的Android系統中,DexOpt有一個問題,也就是這篇文章想要說明並解決的問題。DexOpt會把每一個類的方法id檢索起來,存在一個鏈表結構裡面。但是這個鏈表的長度是用一個short類型來保存的,導致瞭方法id的數目不能夠超過65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。盡管在新版本的Android系統中,DexOpt修復瞭這個問題,但是我們仍然需要對低版本的Android系統做兼容.

目前比較常用的方法:(1) 應用插件化,比如使用我正在參與開發的插件化框架 : https://github.com/singwhatiwanna/dynamic-load-apk ,如果有建議或者相關的問題,歡迎到Github上積極參與. (2) 分割Dex,多工程: 把所需要的.class文件或者是Jar文件和一些源碼一起編譯生成一個Jar文件。然後使用Android SDK提供的dx工具把Jar文件轉成Dex文件。我們可以提前對它進行ODex操作,讓它在被DexClassLoader加載的時候,跳過DexOpt的部分工作,從而加快加載的過程. 這兩種方法並不沖突,插件化除瞭解決應用爆棚,還有很多其他的優點,可以看我之前的文章,不再復述.

當然,Google看來也意識到瞭目前應用方法數爆棚的問題, 目前在已經在API 21中提供瞭通用的解決方案,那就是android-support-multidex.jar. 這個jar包最低可以支持到API 4的版本(Android L及以上版本會默認支持mutidex).

讓我們看一下如何應用android-support-multidex.jar(以下都以在Anroid studio中的使用為例):

 

首先可以用–multi-dex配置(build.gradle)進行解決,生成的Apk中將包含多個dex文件,比如classes.dex, classes2.dex. 如下,需要對build.gradle進行修改:

afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        dx.additionalParameters += '--multi-dex' // enable multidex

        // optional
        // dx.additionalParameters += --main-dex-list=$projectDir/.toString() // enable the main-dex-list
    }
}

但是默認的Dalvik 類加載器隻會尋找classes.dex,所以需要將它們進行合並才能使得被識別

當然,現在有瞭android.support.multidex.jar的支持,一切都會變得非常簡單,首先我們看一下相關源碼的目錄,具體的原理分析我會在之後的文章中進行講解:

android/support/multidex/BuildConfig.class
android/support/multidex/MultiDex$V14.class
android/support/multidex/MultiDex$V19.class
android/support/multidex/MultiDex$V4.class
android/support/multidex/MultiDex.class
android/support/multidex/MultiDexApplication.class
android/support/multidex/MultiDexExtractor$1.class
android/support/multidex/MultiDexExtractor.class
android/support/multidex/ZipUtil$CentralDirectory.class
android/support/multidex/ZipUtil.class

具體集成:
將如下配置加入工程 classPath

repositories {
  jcenter()
}

dependencies {
  compile 'com.google.android:multidex:0.1'
}

接下來集成有兩個步驟:

一. 從sdkextrasandroidsupportmultidexlibrarylibs 目錄將android-support-multidex.jar導入工程中

二. 如果你的工程中已經含有Application類,那麼讓它繼承android.support.multidex.MultiDexApplication類,

如果你的Application已經繼承瞭其他類並且不想做改動,那麼還有另外一種使用方式,覆寫attachBaseContext()方法:

public class MyApplication extends FooApplication {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

最後給出build.gradle中的完整配置:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.13.3'
        classpath 'jp.leafytree.gradle:gradle-android-scala-plugin:1.1'
    }
}

apply plugin: 'com.android.application'
apply plugin: 'jp.leafytree.android-scala'

repositories {
    jcenter()
}

android {
    compileSdkVersion 21
    buildToolsVersion '21.0.2'

    defaultConfig {
        applicationId 'some.app'
        minSdkVersion 19
        targetSdkVersion 19
        versionCode 1
        versionName '1.0'
    }
}

dependencies {
    compile 'com.google.android:multidex:0.1'
    compile 'com.android.support:support-v4:19.0.1'
    compile 'com.google.android.gms:play-services:5.0.77'
    compile 'org.scala-lang:scala-library:2.11.2'
    compile 'org.scaloid:scaloid_2.11:3.4-10'
}

使用MutiDex的主意事項

 

一. 如果你繼承瞭MutiDexApplication或者覆寫瞭Application中的attachBaseContext()方法.

Application類中邏輯的註意事項:

Application 中的靜態全局變量會比MutiDex instal()方法優先加載,所以建議避免在Application類中使用main classes.dex文件中可能使用到的靜態變量,可以根據如下所示的方式進行修改:

@Override
    public void onCreate() {
        super.onCreate();

        final Context mContext = this;
        new Runnable() {

            @Override
            public void run() {
                // put your logic here!
                // use the mContext instead of this here
            }
        }.run();
    }

常見問題

 

DexException: Library dex files are not supported in multi-dex mode,你可能會見到如下的錯誤:

 

 

Error:Execution failed for task ':app:dexDebug'.
> com.android.ide.common.internal.LoggedErrorException: Failed to run command:
    $ANDROID_SDK/build-tools/android-4.4W/dx --dex --num-threads=4 --multi-dex
    ...
  Error Code:
    2
  Output:
    UNEXPECTED TOP-LEVEL EXCEPTION:
    com.android.dex.DexException: Library dex files are not supported in multi-dex mode
        at com.android.dx.command.dexer.Main.runMultiDex(Main.java:322)
        at com.android.dx.command.dexer.Main.run(Main.java:228)
        at com.android.dx.command.dexer.Main.main(Main.java:199)
        at com.android.dx.command.Main.main(Main.java:103)

 

對於dex 的–multi-dex 選項設置與預編譯的library工程有沖突,因此如果你的應用中包含引用的lirary工程,需要將預編譯設置為false:

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

OutOfMemoryError: Java heap space

當運行時如果看到如下錯誤:

UNEXPECTED TOP-LEVEL ERROR:
java.lang.OutOfMemoryError: Java heap space

dexOptions中有一個字段用來增加java堆內存大小:

android {
    // ...
    dexOptions {
        javaMaxHeapSize 2g
    }
}    
        

參考相關資料:

1. MutiDex 官方文檔: https://developer.android.com/reference/android/support/multidex/MultiDex.html

2. https://blog.osom.info/2014/10/multi-dex-to-rescue-from-infamous-65536.html
 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *