天天看點

dex分包方案

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

1. 生成的apk在2.3以前的機器無法安裝,提示install_failed_dexopt

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

conversion to dalvik format failed:unable to execute dex: method id not in [0, 0xffff]: 65536  

出現這種問題的原因是:

1. android2.3及以前版本用來執行dexopt(用于優化dex檔案)的記憶體隻配置設定了5m

2. 一個dex檔案最多隻支援65536個方法。

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

1. 插件化隻适合一些比較獨立的子產品;

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

由于上述問題的存在,通過不斷研究,便有了dex分包的解決方案。簡單來說,其原理是将編譯好的class檔案拆分打包成兩個dex,繞過dex方法數量的限制以及安裝時的檢查,在運作時再動态加載第二個dex檔案中。facebook曾經遇到相似的問題,具體可參考:

<a target="_blank" href="https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920">https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920</a>

文中有這麼一段話:

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分包方案,該方案分為兩步執行:

dex分包方案

整個demo的目錄結構是這樣,我打算将secondactivity,mycontainer以及dropdownview放入第二個dex包中,其它保留在第一個dex包。

一、編譯時分包

整個編譯流程如下:

dex分包方案

除了框出來的兩target,其它都是編譯的标準流程。而這兩個target正是我們的分包操作。首先來看看spliteclasses target。

dex分包方案

由于我們這裡僅僅是一個demo,是以放到第二個包中的檔案很少,就是上面提到的三個檔案。分好包之後就要開始生成dex檔案,首先打包第一個dex檔案: 

dex分包方案

由這裡将${classes}(該檔案夾下都是要打包到第一個dex的檔案)打包生成第一個dex。接着生成第二個dex,并将其打包到資資源檔案中:

dex分包方案

可以看到,此時是将${secclasses}中的檔案打包生成dex,并将其加入ap檔案(打包的資源檔案)中。到此,分包完畢,接下來,便來分析一下如何動态将第二個dex包注入系統的classloader。

二、将dex分包注入classloader

這裡談到注入,就要談到android的classloader體系。

dex分包方案

由上圖可以看出,在葉子節點上,我們能使用到的是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:  

dex分包方案

@override  

protected class&lt;?&gt; findclass(string name) throws classnotfoundexception {  

    list&lt;throwable&gt; suppressedexceptions = new arraylist&lt;throwable&gt;();  

    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:

dex分包方案

public class findclass(string name, list&lt;throwable&gt; 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中,我們執行注入操作:

dex分包方案

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分包方案仍然有幾個注意點:

1. 由于第二個dex包是在application的oncreate中動态注入的,如果dex包過大,會使app的啟動速度變慢,是以,在dex分包過程中一定要注意,第二個dex包不宜過大。

2. 由于上述第一點的限制,假如我們的app越來越臃腫和龐大,往往會采取dex分包方案和插件化方案配合使用,将一些非核心獨立功能做成插件加載,核心功能再分包加載。

繼續閱讀