1. 程式人生 > >徹底解決Android 應用方法數不能超過65536的問題

徹底解決Android 應用方法數不能超過65536的問題

尊重原創 :http://blog.csdn.net/yuanzeyao/article/details/41809423

還可以參考:

作為一名Android開發者,相信你對Android方法數不能超過65536的限制應該有所耳聞,隨著應用程式功能不斷的豐富,總有一天你會遇到一個異常:

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

2、在2.3系統之前,虛擬機器記憶體只分配了5M

知道了原因,我們就來一個個的解決上面的問題,首先對於65k的問題,我們在應用層是無法改變android系統的結構的,所以我們無法將資料型別從short改變為int或者其他型別,也就是說一個dex中的方法數不能超過65k是我們無法逾越的鴻溝,我們只能減少一個dex中的方法數,首先最容易想到的方案就是去掉一些無用的Jar包,以及將一些屬性設定為public,從而可以去掉get/set方法,這種方法只能臨時解決問題,隨著時間的推移,總有一天還是會出現方法數超過65k的,畢竟一個應用一般是在加功能,不會減功能。

下面我來向大家介紹兩種主流的解決方案,一種是以微信為代表的,將一些功能做成外掛,動態載入,另一種方案是以facebook為代表的分包方案,將一個apk中的dex檔案分割成多個dex檔案,然後動態的去載入dex檔案。其實這兩種方案的核心思想是一樣的,外掛是把未來要開發的新功能做成apk和dex動態載入,而分包方案是將已經完成的功能分成多個dex檔案動態載入,其實我個人覺得外掛方案比分包方案更好的解決了65k的問題,因為外掛方案不僅能夠解決65k問題,還能讓我們的應用體積減小,而分包只能解決65k的問題。

關於外掛開發,做成動態載入,我在很早之前一篇文章中就寫過其基本思想,有興趣的同學可以看看

《實現Android 動態載入APK(Fragment or Activity實現)》

http://blog.csdn.net/yuanzeyao/article/details/38565345

下面我們重點介紹分包機制

我們知道一個apk檔案裡面有一個dex檔案,這個dex檔案裡面都是經過優化了的class檔案,所謂分包,就是講一個dex檔案分成多個dex檔案,這裡我們約定一下,第一個dex叫做main.dex,第二個叫做second.dex,通常在分包的時候,我們需要將應用啟動就需要使用的類放入到main.dex中,把不是立馬就需要使用的類放入到second.dex中,對於Android系統,他只會預設載入main.dex的,second.dex對於他來說可能只是一個資原始檔,它是不會主動去載入second.dex,所以我在應用啟動的過程中,我們需要為second.dex建立好一個類載入器,便於我在使用second.dex中的類時,能夠裡面載入該類。

關於如何載入second.dex也有好多做法,用的比較多的主要有一下幾種

1、最簡單的做法就是使用DexClassLoader進行載入,並將該DexClassLoader的父載入器設定為PathClassLoader

2、使用DexClassLoader載入,並將DexClassLoader的父載入器設定成PathClassLoader的父載入器,將PahtClassLoader的父載入器設定成DexClassLoader,仔細品味一下1和2的區別

3、將second.dex的路徑放入到PathClassLoader的載入路徑中

對於第2中方案,在有一種情況下是不能使用的,比如當second.dex通過DexClassLoader載入,但是second.dex中使用了一個類,這個類在main.dex中,這個時候就會丟擲類找不到的異常,所以這種方案只能擁有second.dex不會用到main.dex類的時候

以上說的都是理論,下面我們來實戰一下

我這裡會介紹兩種方案,一種是基於gradle構建Android專案,一種是基於Ant構建Android專案

方案一:基於gradle構建Android專案,並實現分包

環境要求:AndroidStudio0.9以上,gradle外掛0.14.2以上

1、如果你的工程在eclipse中,那麼你需要將該工程匯入到Android中,此時需要你升級adt22以上

2、開啟你工程的build.gradle檔案,檢查gradle外掛是否是0.14.2版本之後,因為0.14.2之後gradle外掛才支援分包


3、開啟工程下某一個Moudle的build.gradle檔案,新增對android-support-multidex.jar的依賴


4、去掉第三方jar包中重複的類


5、設定虛擬機器堆記憶體空間大小,避免在編譯期間OOM


6、gradle構建專案時,貌似預設是不會將so庫加入工程的,所以為了避免此種情況發生,我們需要制定so庫目錄,對於從eclipse轉換過來的工程,還需要制定src和資原始檔路徑


7、如果你的專案依賴了其他庫, 分別在各個庫工程中加入 multiDexEnabled = true 和 jniLibs.srcDirs =['libs']兩個配置即可

8、如果你的專案沒有自定義Application,那麼你在AndroidManifest.xml中使用MultiDexApplication即可,如果你的專案有自定義Application,並且是繼承是Application,那麼只需要改為繼承MultiDexApplication即可,如果你的專案時繼承的其他Application,那麼你需要重寫

attachBaseContext

[java] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. @Override
  2. protectedvoid attachBaseContext(Context base) {  
  3.     super.attachBaseContext(base);  
  4.     MultiDex.install(this);  
  5. }  

經過上述配置,你的專案應該是已經成功分包了。如果分包成功,那麼你解壓你的apk檔案,會發現有兩個dex檔案,通過上述的配置過程,我們發現此方案我們無法控制哪些類在main.dex中,哪些類在second.dex中,通過此種方案配置分包,可以相容API4-API20.其載入second.dex採用的是上述方案中的3


下面我們來看看基於Ant構建Android專案,並實現分包過程

在上述方案中,由於我們無法看到gradle構建專案的指令碼,所以我們無法控制哪些類在第一個dex,哪些類在第二個dex,此方案中,我們採用Ant構建,Ant是允許使用者自己定義構建方案的,比如我們可以通過自定義構建方案,將專案中某些第三方jar包放入到second.dex中,關於這個如何實現,請參考開源專案吧

由於該專案載入second.dex所採用的方案是上述方案2,比如second.dex中的某些第三方jar包依賴main.dex中的某些類,這種方案就會實現,所以在此我將此方案去掉,換成了方案3,也就是將second.dex的路徑設定到PathClassLoader的載入路徑中

我只給出Android 4.4中的解決方案,其他系統大同小異

載入second.dex方法

[java] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. /** 
  2.     @param loader 
  3.          PathClassLoader 
  4.     @additionalClassPathEntries  
  5.          要被載入的dex檔案,這裡就是我們的second.dex 
  6.     @optimizedDirectory 
  7.          就是dex檔案解壓的目錄 
  8. */
  9.  privatestaticvoid install(ClassLoader loader, List<File> additionalClassPathEntries,  
  10.                 File optimizedDirectory)  
  11.                         throws IllegalArgumentException, IllegalAccessException,  
  12.                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException {  
  13.             /* The patched class loader is expected to be a descendant of 
  14.              * dalvik.system.BaseDexClassLoader. We modify its 
  15.              * dalvik.system.DexPathList pathList field to append additional DEX 
  16.              * file entries. 
  17.              */
  18.              //通過反射找到pathList的值
  19.             Field pathListField = findField(loader, "pathList");  
  20.             Object dexPathList = pathListField.get(loader);  
  21.             ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();  
  22.             //將second.dex 加入到PathClassLoader的載入路徑中
  23.             expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,  
  24.                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,  
  25.                     suppressedExceptions));  
  26.             if (suppressedExceptions.size() > 0) {  
  27.                 for (IOException e : suppressedExceptions) {  
  28.                     Log.w(TAG, "Exception in makeDexElement", e);  
  29.                 }  
  30.                 Field suppressedExceptionsField =  
  31.                         findField(loader, "dexElementsSuppressedExceptions");  
  32.                 IOException[] dexElementsSuppressedExceptions =  
  33.                         (IOException[]) suppressedExceptionsField.get(loader);  
  34.                 if (dexElementsSuppressedExceptions == null) {  
  35.                     dexElementsSuppressedExceptions =  
  36.                             suppressedExceptions.toArray(  
  37.                                     new IOException[suppressedExceptions.size()]);  
  38.                 } else {  
  39.                     IOException[] combined =  
  40.                             new IOException[suppressedExceptions.size() +  
  41.                                             dexElementsSuppressedExceptions.length];  
  42.                     suppressedExceptions.toArray(combined);  
  43.                     System.arraycopy(dexElementsSuppressedExceptions, 0, combined,  
  44.                             suppressedExceptions.size(), dexElementsSuppressedExceptions.length);  
  45.                     dexElementsSuppressedExceptions = combined;  
  46.                 }  
  47.                 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);  
  48.             }  
  49.         }  

分包成功後,解壓apk檔案,進入assert資料夾,我們看到如下結構,libs.apk就是第三方jar編譯後形成的dex檔案


對於上面提到的第二個問題INSTALL_FAILED_DEXOPT,根本原因就是2.3版本之前dalvik虛擬機器的記憶體只有5M,所以無論是外掛方案還是分包方案在某些手機上還是會遇到該問題,畢竟我們僅僅是減少了每個dex中包的數量,但是方法總數是沒有減少的,所以解決此問題的根本方法就是修改虛擬機器記憶體至8M,這個需求在Java層是無法實現,但是可以在c層實現,具體實現流程可以參考開源專案:

https://github.com/viilaismonster/LinearAllocFix.git

至於該方法中用到的一些方法,可以到android-support-multidex.jar中找到,這裡就不都貼出來了,如果那裡沒有寫清楚,歡迎留言討論...