【轉】Android開發Dex的分包技術
<p>當一個app的功能越來越複雜,程式碼量越來越多,也許有一天便會突然遇到下列現象:</p>
- 生成的apk在2.3以前的機器無法安裝,提示INSTALL_FAILED_DEXOPT
- 方法數量過多,編譯時出錯,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
<p>出現這種問題的原因是:</p>
- Android2.3及以前版本用來執行dexopt(用於優化dex檔案)的記憶體只分配了5M
- 一個dex檔案最多隻支援65536個方法。
針對上述問題,也出現了諸多解決方案,使用的最多的是外掛化,即將一些獨立的功能做成一個單獨的apk,當開啟的時候使用DexClassLoader動態載入,然後使用反射機制來呼叫外掛中的類和方法。這固然是一種解決問題的方案:但這種方案存在著以下兩個問題:</p>
- 外掛化只適合一些比較獨立的模組;
- 必須通過反射機制去呼叫外掛的類和方法,因此,必須搭配一套外掛框架來配合使用;
由於上述問題的存在,通過不斷研究,便有了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,通過查閱開發文件,我們發現他們有如下使用場景:
- 關於PathClassLoader,文件中寫到: Android uses this class for its system class loader and for its application class loader(s),
由此可知,Android應用就是用它來載入; - DexClass可以載入apk,jar,及dex檔案,但PathClassLoader只能載入已安裝到系統中(即/data/app目錄下)的apk檔案。
知道了兩者的使用場景,下面來分析下具體的載入原理,由上圖可以看到,兩個葉子節點的類都繼承BaseDexClassLoader中,而具體的類載入邏輯也在此類中:
BaseDexClassLoader:
@Override
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;
}
由上述函式可知,當我們需要載入一個class時,實際是從pathList中去需要的,查閱原始碼,發現pathList是DexPathList類的一個例項。ok,接著去分析DexPathList類中的findClass函式,
DexPathList:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
上述函式的大致邏輯為:遍歷一個裝在dex檔案(每個dex檔案實際上是一個DexFile物件)的陣列(Element陣列,Element是一個內部類),然後依次去載入所需要的class檔案,直到找到為止。
看到這裡,注入的解決方案也就浮出水面,假如我們將第二個dex檔案放入Element陣列中,那麼在載入第二個dex包中的類時,應該可以直接找到。
帶著這個假設,來完善demo。
在我們自定義的BaseApplication的onCreate中,我們執行注入操作:
public String inject(String libPath) {
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader)sApplication.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(libPath, sApplication.getDir("dex", 0).getAbsolutePath(), libPath, sApplication.getClassLoader());
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}
這是注入的關鍵函式,分析一下這個函式:
引數libPath是第二個dex包的檔案資訊(包含完整路徑,我們當初將其打包到了assets目錄下),然後將其使用DexClassLoader來載入(這裡為什麼必須使用DexClassLoader載入,回顧以上的使用場景),然後通過反射獲取PathClassLoader中的DexPathList中的Element陣列(已載入了第一個dex包,由系統載入),以及DexClassLoader中的DexPathList中的Element陣列(剛將第二個dex包載入進去),將兩個Element陣列合並之後,再將其賦值給PathClassLoader的Element陣列,到此,注入完畢。
現在試著啟動app,並在TestUrlActivity(在第一個dex包中)中去啟動SecondActivity(在第二個dex包中),啟動成功。這種方案是可行。
但是使用dex分包方案仍然有幾個注意點:</p>
- 由於第二個dex包是在Application的onCreate中動態注入的,如果dex包過大,會使app的啟動速度變慢,因此,在dex分包過程中一定要注意,第二個dex包不宜過大。
- 由於上述第一點的限制,假如我們的app越來越臃腫和龐大,往往會採取dex分包方案和外掛化方案配合使用,將一些非核心獨立功能做成外掛載入,核心功能再分包載入。
更多博文請到作者站點檢視:
Cyning的部落格:Cyning的部落格
相關文章
- 迪斯克Disrupt DEX/系統技術開發/Disrupt DEX開發分析方案
- Disrupt DEX質押分紅系統開發技術方案
- 迪斯克Disrupt DEX系統技術開發詳情分析
- android 基於dex的外掛化開發Android
- 迪斯克DEX質押借貸系統開發技術方案
- 迪斯克(Disrupt DEX)質押眾籌系統開發技術
- 迪斯克|(Disrupt DEX)質押理財系統開發技術分析
- 迪斯克Disrupt DEX眾籌挖礦開發系統搭建技術
- android 開發之 APT 技術AndroidAPT
- 最新的B/S開發技術 (轉)
- Disrupt DEX迪斯克質押挖礦系統/開發python技術Python
- Android開發技術面總結Android
- 迪斯卡(Disrupt DEX)礦池系統開發/資料分析/技術應用
- 迪斯克Disrupt DEX眾籌質押系統開發丨DAPP技術框架APP框架
- Android 分包策略Android
- 聊聊真實的 Android TV 開發技術棧Android
- Android 日常開發總結的技術經驗Android
- OPPO Android開發技術面總結Android
- 短視訊技術詳解:Android端的短視訊開發技術Android
- Android 開發技術週報 Issue#288Android
- Android 開發技術週報 Issue#279Android
- Android 開發有哪些新技術出現?Android
- 迪斯克Disrupt DEX眾籌質押模式系統開發詳情丨技術框架搭建模式框架
- ABAP開發工具及技術概覽(轉)
- 前端開發技術的發展前端
- android日常開發總結的技術經驗60條Android
- Android 日常開發總結的技術經驗 60 條Android
- 對Android開發有用的技術棧(一)架構篇Android架構
- 玩轉iOS開發:iOS開發中的裝逼技術 – RunTime(一)iOS
- 玩轉iOS開發:iOS開發中的裝逼技術 – RunTime(二)iOS
- 玩轉iOS開發:iOS開發中的裝逼技術 - RunTime(二)iOS
- 玩轉iOS開發:iOS開發中的裝逼技術 - RunTime(一)iOS
- Android 開發技術週報 Issue#269Android
- Android開發工程師(雲技術方向)--急聘Android工程師
- Android遊戲開發案例與關鍵技術Android遊戲開發
- Android開發60條技術經驗總結Android
- DEX去中心化交易所|去中心化交易所APP系統定製開發技術中心化APP
- 開發Webview的技術,小的技術知識點WebView