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的配置,支持multidex2.修改你的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,並合並到放在BaseDexClassLoader的DexPathList的 Element數組。

BaseDexClassLoader findClass的過程如下:

	protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; } 

下面代碼為怎么通過DexFile來加載Secondary DEX並放到BaseDexClassLoader的DexPathList中:

	private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); try { if (suppressedExceptions.size() > 0) { for (IOException e : suppressedExceptions) { //Log.w(TAG, "Exception in makeDexElement", e); } Field suppressedExceptionsField = findField(loader, "dexElementsSuppressedExceptions"); IOException[] dexElementsSuppressedExceptions = (IOException[]) suppressedExceptionsField.get(loader); if (dexElementsSuppressedExceptions == null) { dexElementsSuppressedExceptions = suppressedExceptions.toArray( new IOException[suppressedExceptions.size()]); } else { IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length]; suppressedExceptions.toArray(combined); System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length); dexElementsSuppressedExceptions = combined; } suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions); } } catch(Exception e) { } } 

理解官方的分包原理是掌握插件化技術和熱修復技術的基礎。例如:美團Android分包策略就是在MultiDex的基礎上進行了優化,可以在運行時自由的加載Secondary DEX,既能保證冷啟動速度,又能減少運行時的內存占用。

攜程Android App插件化技術參考這兩篇文章:攜程Android App插件化和動態加載實踐 ,再議攜程Android動態加載框架DynamicAPK攜程android中加載類使用的是PathClassLoader。

對於熱修復很多第三方是使用PathClassLoader,PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,Android使用PathClassLoader作為其系統類和應用類的加載器。這個類只能去加載已經安裝到Android系統中的apk文件,DexClassLoader可以用來從.jar和.apk類型的文件內部加載classes.dex文件。可以用來執行非安裝的程序代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM