【轉(zhuǎn)】Android開發(fā)Dex的分包技術(shù)

<p>當(dāng)一個app的功能越來越復(fù)雜骡和,代碼量越來越多,也許有一天便會突然遇到下列現(xiàn)象:</p>

  1. 生成的apk在2.3以前的機器無法安裝件相,提示INSTALL_FAILED_DEXOPT
  2. 方法數(shù)量過多韵卤,編譯時出錯,提示:
    Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

<p>出現(xiàn)這種問題的原因是:</p>

  1. Android2.3及以前版本用來執(zhí)行dexopt(用于優(yōu)化dex文件)的內(nèi)存只分配了5M
  2. 一個dex文件最多只支持65536個方法着撩。

針對上述問題诅福,也出現(xiàn)了諸多解決方案匾委,使用的最多的是插件化,即將一些獨立的功能做成一個單獨的apk氓润,當(dāng)打開的時候使用DexClassLoader動態(tài)加載赂乐,然后使用反射機制來調(diào)用插件中的類和方法。這固然是一種解決問題的方案:但這種方案存在著以下兩個問題:</p>

  1. 插件化只適合一些比較獨立的模塊咖气;
  2. 必須通過反射機制去調(diào)用插件的類和方法挨措,因此,必須搭配一套插件框架來配合使用采章;

由于上述問題的存在运嗜,通過不斷研究,便有了dex分包的解決方案悯舟。簡單來說担租,其原理是將編譯好的class文件拆分打包成兩個dex,繞過dex方法數(shù)量的限制以及安裝時的檢查抵怎,在運行時再動態(tài)加載第二個dex文件中奋救。

faceBook曾經(jīng)遇到相似的問題,具體可參考:
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回調(diào)中被注入到系統(tǒng)的ClassLoader背亥。因此,對于那些在注入之前已經(jīng)引用到的類(以及它們所在的jar),必須放入第一個Dex文件中悬赏。

下面通過一個簡單的demo來講述dex分包方案狡汉,該方案分為兩步執(zhí)行:

image
image

整個demo的目錄結(jié)構(gòu)是這樣,我打算將SecondActivity闽颇,MyContainer以及DropDownView放入第二個dex包中盾戴,其它保留在第一個dex包。

1.編譯時分包

整個編譯流程如下:

image
image

除了框出來的兩Target兵多,其它都是編譯的標準流程尖啡。而這兩個Target正是我們的分包操作。首先來看看spliteClasses target剩膘。


image
image

由于我們這里僅僅是一個demo,因此放到第二個包中的文件很少衅斩,就是上面提到的三個文件。分好包之后就要開始生成dex文件怠褐,首先打包第一個dex文件:

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

image
image

可以看到,此時是將${secclasses}中的文件打包生成dex具温,并將其加入ap文件(打包的資源文件)中蚕涤。到此,分包完畢铣猩,接下來揖铜,便來分析一下如何動態(tài)將第二個dex包注入系統(tǒng)的ClassLoader。

2.將dex分包注入ClassLoader

這里談到注入达皿,就要談到Android的ClassLoader體系天吓。

image
image

由上圖可以看出,在葉子節(jié)點上峦椰,我們能使用到的是DexClassLoader和PathClassLoader龄寞,通過查閱開發(fā)文檔,我們發(fā)現(xiàn)他們有如下使用場景:

  1. 關(guān)于PathClassLoader汤功,文檔中寫到: Android uses this class for its system class loader and for its application class loader(s),
    由此可知物邑,Android應(yīng)用就是用它來加載;
  2. DexClass可以加載apk,jar,及dex文件,但PathClassLoader只能加載已安裝到系統(tǒng)中(即/data/app目錄下)的apk文件滔金。

知道了兩者的使用場景色解,下面來分析下具體的加載原理,由上圖可以看到餐茵,兩個葉子節(jié)點的類都繼承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;  
}  

由上述函數(shù)可知,當(dāng)我們需要加載一個class時忿族,實際是從pathList中去需要的锣笨,查閱源碼,發(fā)現(xiàn)pathList是DexPathList類的一個實例道批。ok错英,接著去分析DexPathList類中的findClass函數(shù),
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;  
}  

上述函數(shù)的大致邏輯為:遍歷一個裝在dex文件(每個dex文件實際上是一個DexFile對象)的數(shù)組(Element數(shù)組屹徘,Element是一個內(nèi)部類),然后依次去加載所需要的class文件衅金,直到找到為止噪伊。
看到這里,注入的解決方案也就浮出水面氮唯,假如我們將第二個dex文件放入Element數(shù)組中鉴吹,那么在加載第二個dex包中的類時,應(yīng)該可以直接找到惩琉。
帶著這個假設(shè)豆励,來完善demo。
在我們自定義的BaseApplication的onCreate中,我們執(zhí)行注入操作:

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";  
}   

這是注入的關(guān)鍵函數(shù)良蒸,分析一下這個函數(shù):
參數(shù)libPath是第二個dex包的文件信息(包含完整路徑技扼,我們當(dāng)初將其打包到了assets目錄下),然后將其使用DexClassLoader來加載(這里為什么必須使用DexClassLoader加載嫩痰,回顧以上的使用場景)剿吻,然后通過反射獲取PathClassLoader中的DexPathList中的Element數(shù)組(已加載了第一個dex包,由系統(tǒng)加載)串纺,以及DexClassLoader中的DexPathList中的Element數(shù)組(剛將第二個dex包加載進去)丽旅,將兩個Element數(shù)組合并之后,再將其賦值給PathClassLoader的Element數(shù)組纺棺,到此榄笙,注入完畢。

現(xiàn)在試著啟動app祷蝌,并在TestUrlActivity(在第一個dex包中)中去啟動SecondActivity(在第二個dex包中)茅撞,啟動成功。這種方案是可行杆逗。

但是使用dex分包方案仍然有幾個注意點:</p>

  1. 由于第二個dex包是在Application的onCreate中動態(tài)注入的乡翅,如果dex包過大,會使app的啟動速度變慢罪郊,因此蠕蚜,在dex分包過程中一定要注意,第二個dex包不宜過大悔橄。
  2. 由于上述第一點的限制靶累,假如我們的app越來越臃腫和龐大,往往會采取dex分包方案和插件化方案配合使用癣疟,將一些非核心獨立功能做成插件加載挣柬,核心功能再分包加載。

更多博文請到作者站點查看:

Cyning的博客:Cyning的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睛挚,一起剝皮案震驚了整個濱河市邪蛔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扎狱,老刑警劉巖侧到,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異淤击,居然都是意外死亡匠抗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門污抬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汞贸,“玉大人,你說我怎么就攤上這事∈改澹” “怎么了门驾?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長踏堡。 經(jīng)常有香客問我猎唁,道長,這世上最難降的妖魔是什么顷蟆? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任诫隅,我火速辦了婚禮,結(jié)果婚禮上帐偎,老公的妹妹穿的比我還像新娘逐纬。我一直安慰自己,他們只是感情好削樊,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布豁生。 她就那樣靜靜地躺著,像睡著了一般漫贞。 火紅的嫁衣襯著肌膚如雪甸箱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天迅脐,我揣著相機與錄音芍殖,去河邊找鬼。 笑死谴蔑,一個胖子當(dāng)著我的面吹牛豌骏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播隐锭,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼窃躲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钦睡?” 一聲冷哼從身側(cè)響起蒂窒,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎荞怒,沒想到半個月后洒琢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡挣输,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年纬凤,在試婚紗的時候發(fā)現(xiàn)自己被綠了福贞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撩嚼。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出完丽,到底是詐尸還是另有隱情恋技,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布逻族,位于F島的核電站蜻底,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏聘鳞。R本人自食惡果不足惜薄辅,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抠璃。 院中可真熱鬧站楚,春花似錦、人聲如沸搏嗡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽采盒。三九已至旧乞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間磅氨,已是汗流浹背尺栖。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留悍赢,地道東北人决瞳。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像左权,于是被迫代替她去往敵國和親皮胡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容