Android分包原理

bjfw5477 8年前發布 | 34K 次閱讀 安卓開發 Android開發 移動開發

來自: http://souly.cn/技術博文/2016/02/25/android分包原理/

如果App引用的庫太多,方法數超過65536后無法編譯。這是因為單個dex里面不能有超過65536個方法。為什么有最大的限制呢, 因為android會把每一個類的方法id檢索起來,存在一個鏈表結構里面。但是這個鏈表的長度是用一個short類型來保存的, short占兩個字節(保存-2的15次方到2的15次方-1,即-32768~32767),最大保存的數量就是65536。新版本的Android系統中修復了這個問題, 但是我們仍然需要對低版本的Android系統做兼容.

解決方法有如下幾個: 1.精簡方法數量,刪除沒用到的類、方法、第三方庫。 2.使用ProGuard去掉一些未使用的代碼 3.復雜模塊采用jni的方式實現,也可以對邊緣模塊采用本地插件化的方式。 4.分割Dex

本文只介紹最后一種方法。

multidex方案配置

dex文件拆成兩個或多個,為此谷歌官方推出了multidex兼容包,配合AndroidStudio實現了一個APK包含多個dex的功能。 Android 的 Gradle插件在 Android Build Tool 21.1開始就支持使用multidex了。

使用步驟

使用步驟包括: 1.修改Gradle的配置,支持multidex 2.修改你的manifest。讓其支持multidexapplication類

注意其中第二步其實還有另外兩種替代方法,下面介紹。

修改Gradle的配置,支持multidex:

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.0"
    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 21
        ...
        // Enabling multidex support.         multiDexEnabled true
    }
    ...
}
dependencies {
  compile 'com.android.support:multidex:1.0.0'
}
    

你可以在Gradle配置文件中的defaultConfig、 buildType、productFlavor中設置 multiDexEnabled true。

在manifest文件中,在application標簽下添加MultidexApplication Class的引用,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.multidex.myapplication">
    <application
        ...
        android:name="android.support.multidex.MultiDexApplication">
        ...
    </application>
</manifest>
    

當然,如果你重寫了Application,可以讓自定義Applicationd繼承android.support.multidex.MultiDexApplication。

如果之前已經繼承了其他Application類,可以重寫attachBaseContext()方法,并添加語句MultiDex.install(this);如下:

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

注意事項:Application 中的靜態全局變量會比MutiDex的 instal()方法優先加載,所以建議避免在Application類中使用靜態變量引用 main classes.dex文件以外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();

    }

    

Multidex的局限性

官方文檔中提到了Multidex有局限性:

1.如果第二個(或其他個)dex文件很大的話,安裝.dex文件到data分區時可能會導致ANR(應用程序無響應),此時應該使用ProGuard減小DEX文件的大小。 2.由于Dalvik linearAlloc的bug的關系,使用了multidex的應用可能無法在Android 4.0 (API level 14)或之前版本的設備上運行。 3.由于Dalvik linearAlloc的限制,使用了multidex的應用會請求非常大的內存分配,從而導致程序奔潰。Dalvik linearAlloc是一個固定大小的緩沖區。 在應用的安裝過程中,系統會運行一個名為dexopt的程序為該應用在當前機型中運行做準備。dexopt使用LinearAlloc來存儲應用的方法信息。 Android 2.2和2.3的緩沖區只有5MB,Android 4.x提高到了8MB或16MB。當方法數量過多導致超出緩沖區大小時,會造成dexopt崩潰。 4.在Dalvik運行時中,某些類的方法必須要放在主dex中,Android構建工具可能無法確保所有有此要求的類被編譯進主dex中。

這些問題也非常值得我們關注.

一些在二級Dex加載之前,可能會被調用到的類(比如靜態變量的類),需要放在主Dex中.否則會ClassNotFoundError. 通過修改Gradle,可以顯式的把一些類放在Main Dex中.

afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        dx.additionalParameters += '--multi-dex'
        dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
    }
}
    

上面是修改后的Gradle,其中 是一個文本文件的文件名,存放在和這個Gradle腳本同一級的文件目錄下. 而這個文本文件的內容如下.實際就是把需要放在Main Dex的類羅列出來.

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
    

project.afterEvaluate標簽在特定的project配置完成后運行,而gradle.projectsEvaluated在所有projects配置完成后運行。

如果用使用其他Lib,要保證這些Lib沒有被preDex,否則可能會拋出下面的異常:

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:337)
        at com.android.dx.command.dexer.Main.run(Main.java:243)
        at com.android.dx.command.dexer.Main.main(Main.java:214)
        at com.android.dx.command.Main.main(Main.java:106)
    

遇到這個異常,需要在Gradle中修改,讓它不要對Lib做preDexing

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

如果每次都打開MultiDex編譯版本的話,會比平常用更多的時間. Android的官方文檔也給了我們一個小小的建議,利用Gradle建立兩個Flavor.一個minSdkVersion設置成21,這是用了ART支持的Dex格式, 避免了MultiDex的開銷.而另外一個Flavor就是原本支持的最小sdkVersion.平時開發時候調試程序,就用前者的Flavor, 發布版本打包就用后者的Flavor.

android {
    productFlavors {
        // Define separate dev and prod product flavors.         dev {
            // dev utilizes minSDKVersion = 21 to allow the Android gradle plugin             // to pre-dex each module and produce an APK that can be tested on             // Android Lollipop without time consuming dex merging processes.             minSdkVersion 21
        }
        prod {
            // The actual minSdkVersion for the application.             minSdkVersion 14
        }
    }
          ...
    buildTypes {
        release {
            runProguard true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
  compile 'com.android.support:multidex:1.0.0'
}
    

MultiDex實現原理

下面從DEX自動拆包和動態加載兩方面來分析。

1.Dex 拆分

dex拆分步驟分為:

1)自動掃描整個工程代碼得到main-dex-list; 2)根據main-dex-list對整個工程編譯后的所有class進行拆分,將主、從dex的class文件分開; 3)用dx工具對主、從dex的class文件分別打包成 .dex文件,并放在apk的合適目錄。

怎么自動生成 main-dex-list? Android SDK 從 build tools 21 開始提供了 mainDexClasses 腳本來生成主 dex 的文件列表。查看這個腳本的源碼,可以看到它主要做了下面兩件事情:

1)調用 proguard 的 shrink 操作來生成一個臨時 jar 包; 2)將生成的臨時 jar 包和輸入的文件集合作為參數,然后調用com.android.multidex.MainDexListBuilder 來生成主 dex 文件列表。

Proguard的官網執行步驟如下:

在 shrink 這一步,proguard 會根據 keep 規則保留需要的類和類成員,并丟棄不需要的類和類成員。也就是說,上面 shrink 步驟生成的臨時 jar 包里面保留了符合 keep 規則的類,這些類是需要放在主 dex 中的入口類。

但是僅有這些入口類放在主 dex 還不夠,還要找出入口類引用的其他類,不然仍然會在啟動時出現 NoClassDefFoundError。而找出這些引用類,就是調用的 com.android.multidex.MainDexListBuilder,它的部分核心代碼如下:

在調用 com.android.multidex.MainDexListBuilder 之后,符合 keep 規則的主 dex 文件列表就生成了。

2.Dex加載

因為Android系統在啟動應用時只加載了主dex(Classes.dex),其他的 dex 需要我們在應用啟動后進行動態加載安裝。 Google 官方方案是如何加載的呢,Google官方支持Multidex 的 jar 包是 android-support-multidex.jar,該 jar 包從 build tools 21.1 開始支持。這個 jar 加載 apk 中的從 dex 流程如下:

此處主要的工作就是從 apk 中提取出所有的從 dex(classes2.dex,classes3.dex,…),然后通過反射依次安裝加載從 dex 并合并 DexPathList 的 Element 數組。

 本文由用戶 bjfw5477 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!