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 數組。