Android dex分包方案

當一個app的功能愈來愈複雜,代碼量愈來愈多,也許有一天便會忽然遇到下列現象java

1. 生成的apk在2.3之前的機器沒法安裝,提示INSTALL_FAILED_DEXOPTandroid

2. 方法數量過多,編譯時出錯,提示:數組

Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536  app

 

出現這種問題的緣由是框架

1. Android2.3及之前版本用來執行dexopt(用於優化dex文件)的內存只分配了5Mide

2. 一個dex文件最多隻支持65536個方法。函數

 

針對上述問題,也出現了諸多解決方案,使用的最多的是插件化,即將一些獨立的功能作成一個單獨的apk,當打開的時候使用DexClassLoader動態加載,而後使用反射機制來調用插件中的類和方法。這當然是一種解決問題的方案:但這種方案存在着如下兩個問題:優化

1. 插件化只適合一些比較獨立的模塊;this

2. 必須經過反射機制去調用插件的類和方法,所以,必須搭配一套插件框架來配合使用;spa

 

因爲上述問題的存在,經過不斷研究,便有了dex分包的解決方案。簡單來講,其原理是將編譯好的class文件拆分打包成兩個dex,繞過dex方法數量的限制以及安裝時的檢查,在運行時再動態加載第二個dex文件中。faceBook曾經遇到類似的問題,具體可參考:

https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920

文中有這麼一段話:

However, there was no way we could break our app up this way--too many of our classes are accessed directly by the Android framework. Instead, we needed to inject our secondary dex files directly into the system class loader。

文 中說得比較簡單,咱們來完善一下該方案:除了第一個dex文件(即正常apk包惟一包含的Dex文件),其它dex文件都以資源的方式放在安裝包中,並在 Application的onCreate回調中被注入到系統的ClassLoader。所以,對於那些在注入以前已經引用到的類(以及它們所在的 jar),必須放入第一個Dex文件中。

 

下面經過一個簡單的demo來說述dex分包方案,該方案分爲兩步執行:


整個demo的目錄結構是這樣,我打算將SecondActivity,MyContainer以及DropDownView放入第二個dex包中,其它保留在第一個dex包。

1、編譯時分包

整個編譯流程以下:


 

除了框出來的兩Target,其它都是編譯的標準流程。而這兩個Target正是咱們的分包操做。首先來看看spliteClasses target。


 

因爲咱們這裏僅僅是一個demo,所以放到第二個包中的文件不多,就是上面提到的三個文件。分好包以後就要開始生成dex文件,首先打包第一個dex文件: 


 

由這裏將${classes}(該文件夾下都是要打包到第一個dex的文件)打包生成第一個dex。接着生成第二個dex,並將其打包到資資源文件中:


 

能夠看到,此時是將${secclasses}中的文件打包生成dex,並將其加入ap文件(打包的資源文件)中。到此,分包完畢,接下來,便來分析一下如何動態將第二個dex包注入系統的ClassLoader。

 

2、將dex分包注入ClassLoader

這裏談到注入,就要談到Android的ClassLoader體系。

 


由上圖能夠看出,在葉子節點上,咱們能使用到的是DexClassLoader和PathClassLoader,經過查閱開發文檔,咱們發現他們有以下使用場景:

1. 關於PathClassLoader,文檔中寫到: Android uses this class for its system class loader and for its application class loader(s),

由此可知,Android應用就是用它來加載;

2. DexClass能夠加載apk,jar,及dex文件,但PathClassLoader只能加載已安裝到系統中(即/data/app目錄下)的apk文件。

 

知道了二者的使用場景,下面來分析下具體的加載原理,由上圖能夠看到,兩個葉子節點的類都繼承BaseDexClassLoader中,而具體的類加載邏輯也在此類中:

BaseDexClassLoader:  

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. @Override  

  2. protected Class<?> findClass(String name) throws ClassNotFoundException {  

  3.     List<Throwable> suppressedExceptions = new ArrayList<Throwable>();  

  4.     Class c = pathList.findClass(name, suppressedExceptions);  

  5.     if (c == null) {  

  6.         ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);  

  7.         for (Throwable t : suppressedExceptions) {  

  8.             cnfe.addSuppressed(t);  

  9.        }  

  10.         throw cnfe;  

  11.     }  

  12.      return c;  

  13. }  

 

由上述函數可知,當咱們須要加載一個class時,實際是從pathList中去須要的,查閱源碼,發現pathList是DexPathList類的一個實例。ok,接着去分析DexPathList類中的findClass函數,

DexPathList:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public Class findClass(String name, List<Throwable> suppressed) {  

  2.     for (Element element : dexElements) {  

  3.         DexFile dex = element.dexFile;  

  4.         if (dex != null) {  

  5.             Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);  

  6.             if (clazz != null) {  

  7.                 return clazz;  

  8.             }  

  9.         }  

  10.    }  

  11.     if (dexElementsSuppressedExceptions != null) {  

  12.         suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));  

  13.     }  

  14.     return null;  

  15. }  

上述函數的大體邏輯爲:遍歷一個裝在dex文件(每一個dex文件其實是一個DexFile對象)的數組(Element數組,Element是一個內部類),而後依次去加載所須要的class文件,直到找到爲止。

看到這裏,注入的解決方案也就浮出水面,假如咱們將第二個dex文件放入Element數組中,那麼在加載第二個dex包中的類時,應該能夠直接找到。

帶着這個假設,來完善demo。

在咱們自定義的BaseApplication的onCreate中,咱們執行注入操做:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public String inject(String libPath) {  

  2.     boolean hasBaseDexClassLoader = true;  

  3.     try {  

  4.         Class.forName("dalvik.system.BaseDexClassLoader");  

  5.     } catch (ClassNotFoundException e) {  

  6.         hasBaseDexClassLoader = false;  

  7.     }  

  8.     if (hasBaseDexClassLoader) {  

  9.         PathClassLoader pathClassLoader = (PathClassLoader)sApplication.getClassLoader();  

  10.         DexClassLoader dexClassLoader = new DexClassLoader(libPath, sApplication.getDir("dex"0).getAbsolutePath(), libPath, sApplication.getClassLoader());  

  11.         try {  

  12.             Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));  

  13.             Object pathList = getPathList(pathClassLoader);  

  14.             setField(pathList, pathList.getClass(), "dexElements", dexElements);  

  15.             return "SUCCESS";  

  16.         } catch (Throwable e) {  

  17.             e.printStackTrace();  

  18.             return android.util.Log.getStackTraceString(e);  

  19.         }  

  20.     }  

  21.     return "SUCCESS";  

  22. }   

這是注入的關鍵函數,分析一下這個函數:

參 數libPath是第二個dex包的文件信息(包含完整路徑,咱們當初將其打包到了assets目錄下),而後將其使用DexClassLoader來加 載(這裏爲何必須使用DexClassLoader加載,回顧以上的使用場景),而後經過反射獲取PathClassLoader中的 DexPathList中的Element數組(已加載了第一個dex包,由系統加載),以及DexClassLoader中的DexPathList中 的Element數組(剛將第二個dex包加載進去),將兩個Element數組合並以後,再將其賦值給PathClassLoader的Element 數組,到此,注入完畢。

 

如今試着啓動app,並在TestUrlActivity(在第一個dex包中)中去啓動SecondActivity(在第二個dex包中),啓動成功。這種方案是可行。

 

可是使用dex分包方案仍然有幾個注意點:

1. 因爲第二個dex包是在Application的onCreate中動態注入的,若是dex包過大,會使app的啓動速度變慢,所以,在dex分包過程當中必定要注意,第二個dex包不宜過大。

2. 因爲上述第一點的限制,假如咱們的app愈來愈臃腫和龐大,每每會採起dex分包方案和插件化方案配合使用,將一些非核心獨立功能作成插件加載,核心功能再分包加載。

 

原文:http://my.oschina.net/853294317/blog/308583?fromerr=BMNU2kai

相關文章
相關標籤/搜索