出現的原因:
Android 5.0 之前版本的 Dalvik 可執行文件分包支持
Android 5.0(API 級別 21)之前的平台版本使用 Dalvik 運行時來執行應用代碼。默認情況下,Dalvik 限制應用的每個 APK 只能使用單個 classes.dex
字節碼文件。要想繞過這一限制,您可以使用 MultiDex,它會成為您的應用主要 DEX 文件的一部分,然后管理對其他 DEX 文件及其所包含代碼的訪問。
當Android系統安裝一個應用的時候,有一步是對Dex進行優化,這個過程有一個專門的工具來處理,叫DexOpt。DexOpt的執行過程是在第一次加載Dex文件的時候執行的。這個過程會生成一個ODEX文件,即Optimised Dex。執行ODex的效率會比直接執行Dex文件的效率要高很多。
但是在早期的Android系統中,DexOpt有一個問題,DexOpt會把每一個類的方法id檢索起來,存在一個鏈表結構里面。但是這個鏈表的長度是用一個short類型來保存的,導致了方法id的數目不能夠超過65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。盡管在新版本的Android系統中,DexOpt修復了這個問題,但是我們仍然需要對低版本的Android系統做兼容。
為了解決方法數超限的問題,需要將該dex文件拆成兩個或多個,為此谷歌官方推出了multidex兼容包,配合AndroidStudio實現了一個APK包含多個dex的功能。
Android 5.0 及更高版本的 Dalvik 可執行文件分包支持
Android 5.0(API 級別 21)及更高版本使用名為 ART 的運行時,后者原生支持從 APK 文件加載多個 DEX 文件。ART 在應用安裝時執行預編譯,掃描 classesN.dex
文件,並將它們編譯成單個 .oat
文件,供 Android 設備執行。因此,如果您的 minSdkVersion
為 21 或更高值,則不需要 Dalvik 可執行文件分包支持庫。
如需了解有關 Android 5.0 運行時的詳細信息,請參閱 ART 和 Dalvik。
目前在已經在API 21中提供了通用的解決方案,那就是android-support-multidex.jar. 這個jar包最低可以支持到API 4的版本(Android L及以上版本會默認支持mutidex).
引起的錯誤:
Android方法數不能超過65K的限制:
Conversion to Dalvik format failed:Unable toexecute dex: method ID not in [0, 0xffff]: 65536
可能有些同學會說,解決這個問題很簡單,我們只需要在Project.proterty中配置一句話就Ok啦,
dex.force.jumbo=true
是的,加入了這句話,確實可以讓你的應用通過編譯,但是在一些2.3系統的機器上很容易出現
INSTALL_FAILED_DEXOPT異常
對於以上兩個異常,我們先來分析一下原因:
1、Android系統中,一個Dex文件中存儲方法id用的是short類型數據,所以導致你的dex中方法不能超過65k
.Java文件編譯生成字節碼文件,然后打包生成.dex時會按照類,方法等進行分類,使用IndexMap結構處理。
所有的方法都存放在short[] 里, 而short的大小正是65K。
method的數量遠多於class的數量,method的數量會先到65K,class、field同樣存在65k的大小限制。
統計時是把系統的,第三方的加上自己的,方法數量的合,都放到這個數組里進行計算, frameworkMethodids + libraryMdthodids + mycodemethodids。
2、在2.3系統之前,虛擬機內存只分配了5M
使用:
multidex是一個文檔齊全的成熟的解決方案。強烈推薦遵循安卓開發者網站上的指示來啟用multidex。也可以參考github上的項目樣例。
在app/build.gradle 下,添加:
1 compile 'com.android.support:multidex:1.0.1'
在application中:(沒有繼承其他application的,使用MultidexApplication,如果繼承了其他application的,就用如下方式加載)
1 @Override 2 protected void attachBaseContext(Context base) { 3 super.attachBaseContext(base); 4 //因為引用的包過多,實現多包問題 5 MultiDex.install(this); 6 }
Multidex的方式的局限性:
(1)如果DEX文件太大,安裝分割dex文件是一個復雜的過程,可能會導致應用程序無響應(ANR)的錯誤。在這種情況下,你應該盡量的減小dex文件的大小和刪除無用的邏輯,而不是完全依賴於multidex。
(2)在Android 4.0設備(API Level 14)之前,由於Dalvik linearalloc bug(問題22586),multidex很可能是無法運行的。如果希望運行在Level 14之前的Android系統版本,請先確保完整的測試和使用。
(3)應用程序使用了multiedex,需要申請很多的一塊內存,會造成使用比較大的內存,當然,可能還會引起dalvik虛擬機的崩潰(issue 78035)。
(4)對於應用程序比較復雜的,存在較多的library的項目。multidex可能會造成不同依賴項目間的dex文件函數相互調用,找不到方法。
聲明主 DEX 文件中需要的類
為 Dalvik 可執行文件分包構建每個 DEX 文件時,構建工具會執行復雜的決策制定來確定主要 DEX 文件中需要的類,以便應用能夠成功啟動。如果啟動期間需要的任何類未在主 DEX 文件中提供,那么您的應用將崩潰並出現錯誤 java.lang.NoClassDefFoundError
。
該情況不應出現在直接從應用代碼訪問的代碼上,因為構建工具能識別這些代碼路徑,但可能在代碼路徑可見性較低(如使用的庫具有復雜的依賴項)時出現。例如,如果代碼使用自檢機制或從原生代碼調用 Java 方法,那么這些類可能不會被識別為主 DEX 文件中的必需項。
因此,如果您收到 java.lang.NoClassDefFoundError
,則必須使用構建類型中的 multiDexKeepFile
或 multiDexKeepProguard
屬性聲明它們,以手動將這些其他類指定為主 DEX 文件中的必需項。如果類在 multiDexKeepFile
或 multiDexKeepProguard
文件中匹配,則該類會添加至主 DEX 文件。
multiDexKeepFile 屬性
您在 multiDexKeepFile
中指定的文件應該每行包含一個類,並且采用 com/example/MyClass.class
的格式。例如,您可以創建一個名為 multidex-config.txt
的文件,如下所示:
1 com/example/MyClass.class 2 com/example/MyOtherClass.class
然后,您可以按以下方式針對構建類型聲明該文件:
1 android { 2 buildTypes { 3 release { 4 multiDexKeepFile file 'multidex-config.txt' 5 ... 6 } 7 } 8 }
請記住,Gradle 會讀取相對於 build.gradle
文件的路徑,因此如果 multidex-config.txt
與 build.gradle
文件在同一目錄中,以上示例將有效。
解決ANR的方法:
總體思路是:把這個MultiDex.install(this)放到異步線程中加載,同時又要保證在用戶使用功能之前,異步線程已經把非主dex加載完成。
從編譯到運行起來,發生什么了呢?
1. 編譯生成 allclasses.jar 生成classes.dex, classes2.dex, .... 多個。
安裝階段:把.dex 優化成classes.odex, 注意,只優化第一個dex文件。
首次運行:
執行MultiDex.install()必然會再次對classes2.dex執行dexopt等操作,所有這些操作必須在5秒內完成,否則就ANR。
非首次啟動則直接從cache中讀取已經執行過dexopt的ODEX文件,這個過程對啟動並無太大影響
1. dexpath BaseClassLoader.pathlist 類, 把所有的dex都加載。解決了為什么多個dex里的方法也能被正確的index到,不會導致no such method 的crash
2. odex, 這個過程是非常耗時的,如果第二個dex是5-7M的話大約要加載4s, google這個默認方式時間太長,會導致ANR。
這就需要完成2個事情:
1. 把啟動需要加載的類放到主dex中。
2. 顯示loading界面,保證用戶使用之前把dex加載完成。
主dex中的類:
把這個MultiDex.install(this)放到異步線程中加載,主dex中存放application啟動的1直接引用類,所有可能的2入口類,加上3第三方的調用。掃面這些類的直接引用類。統統放到主dex中。
首先在編譯打包階段,在build task中添加一層task, 添加Blacklist.xml自定義的黑名單, 這個名單中的方法對應的類需要添加到主dex中,保證主dex能夠加載APP啟動時需要的必要的類。
balcklist中包含了需要放到主dex中的類的path,addtask的這個task,把blacklist中的類放到了dextask中生成想要的dex。
如何確定哪些類需要放到blcaklist中:
粒度到方法層面,從主activity加載開始,加載了class B的public x 和public y, 則把B的x 和 y方法中,僅x, y方法中import到的類進行分析, 進行回溯添加。
那如何能讓工作更高效,能讓查找更有保證呢? 請看下篇,基於字節碼文件分析.class文件。
異步加載界面的現實時機:
splash界面,主界面,在收到事件之前進行監聽顯示load界面。直到異步加載完成所有dex。
其他問題:
主dex不夠65k方法,主dex會有多少方法? 生成dex時會一直塞直到塞滿65K。
JNI層的回調類,和用到反射的類是需要放到主dex中的,這些BI,庫加載都是需要運行就work的。
參考:
第三方庫 TurboDex https://github.com/asLody/TurboDex
Android MultiDex 實現原理解析 (配置和簡要原理)
Android Dex 分包指南 (Gradle build task)
Android的multidex帶來的性能問題-減慢app啟動速度 (解決multidex app啟動性能問題)