1 問題
在Android系統中,一個App的全部代碼都在一個Dex文件裏面。Dex是一個相似Jar的存
儲了多有Java編譯字節碼的歸檔文件。由於Android系統使用Dalvik虛擬機,因此須要把
使用Java Compiler編譯以後的class文件轉換成Dalvik可以執行的class文件。這裏須要強
調的是,Dex和Jar同樣是一個歸檔文件,裏面仍然是Java代碼對應的字節碼文件。
當Android系統啓動一個應用的時候,有一步是對Dex進行優化,這個過程有一個專門的
工具來處理,叫DexOpt。DexOpt的執行過程是在第一次加載Dex文件的時候執行的。這
個過程會生成一個ODEX文件,即Optimised Dex。執行ODex的效率會比直接執行Dex文
件的效率要高不少。可是在早期的Android系統中,DexOpt有一個問題,也就是這篇文
章想要說明並解決的問題。DexOpt會把每個類的方法id檢索起來,存在一個鏈表結構
裏面。可是這個鏈表的長度是用一個short類型來保存的,致使了方法id的數目不可以超過
65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。儘管在新版本的
Android系統中,DexOpt修復了這個問題,可是咱們仍然須要對老系統作兼容。
2 思路
一種有效的解決思路是把Dex文件分割成多個較小的Dex。這就如同不少項目會把本身分
割成多個Jar文件同樣,不一樣的功能在不一樣的Jar文件裏面,經過一些配置和額外的操做,
可讓虛擬機有選擇性的加載Jar文件。可是在Android系統中,一個應用是隻容許有一
個Dex文件的。也就是說在編譯期的時候,全部的Jar文件最終會被合併成一個Dex文件。
咱們沒有辦法在Apk文件裏面打包兩個Dex,讓DexOpt分別對兩個Dex文件作處理,而
Android系統也不會同時爲一個Apk加載兩個Dex。
1
2.1 動態加載
若是咱們把Dex分紅多個文件,而後在程序運行的時候,再把多的那幾個動態的加載進來
是否可行呢?也就是說咱們可否在運行時階段把代碼加入虛擬機中。對於虛擬機來講,其
實全部的代碼都是在運行時被加載進來的。而不一樣於C語言還存在着靜態連接。虛擬機在
全部Java代碼執行以前被啓動,而後開始把字節碼加載到環境中執行,咱們能夠理解成所
有的代碼都是動態加載到虛擬機裏的。
而說到加載,不得不說的是ClassLoader。它的工做就是加載.class文件。在Android的
Dalvik環境中,對應的是DexClassLoader,它們的功能是徹底同樣的。ClassLoader的
一大特色就是它是一個樹狀結構。每一個ClassLoader都有一個父親ClassLoader。也就是
說,ClassLoader不是把全部的Class放到一個巨大的數組或別的什麼數據結構中來處理。
ClassLoader在加載一個Jar中的類的時候,須要制定另外一個ClassLoader做爲父親節點,
當咱們須要經過ClassLoader獲得一個類類型的時候,ClassLoader會把請求優先交給父
親ClassLoader來處理,而父親ClassLoader又會交給它的父親,一直到根ClassLoader。
若是根ClassLoader有這個類,而返回這個類的類類型,不然把這個請求交給這個請求的
來源子ClassLoader。這是一種向上傳遞,向下分發的機制。這種狀況下,對於調用着來
說,子ClassLoader永遠都是包含最多Class的ClassLoader。有一點咱們須要注意,父親
ClassLoader只會向請求來源分發本身的處理結果。因此若是來源是本身,那麼若是沒有
請求類它就會返回空,而不是遍歷全部子ClassLoader去請求是否有被請求的類。
在Android系統中,對於一個應用來講,其實有兩個ClassLoader,一個是SystemClassLoader,這個ClassLoader裏面除了Java標準的類庫以外,還有一個android.jar,全部
Android Framework層的類都在這裏。而另一個重要的ClassLoader就是基於Android
Context的ClassLoader。全部屬於當前應用的類都是用這個ClassLoader來加載的,咱們
能夠在Android源碼中看到,全部的Activity,Service,View都是使用這個ClassLoader
來反射並建立的。咱們暫時把它叫作ContextClassLoader。
3 加載外部Dex
3.1 構建一個Dex文件
這一步並不複雜,首先咱們把所須要的.class文件或者是Jar文件和一些源碼一塊兒編譯生
成一個Jar文件。而後使用Android SDK提供的dx工具把Jar文件轉成Dex文件。咱們能夠
提早對它進行ODex操做,讓它在被DexClassLoader加載的時候,跳過DexOpt的部分工
做,從而加快加載的過程。
2
3.2 DexClassLoader
如今的工做就是在運行時加載這個Dex文件了。咱們能夠在Application啓動的onCreate
方法裏面加載Dex,可是若是你的Dex太大,那麼它會讓你的App啓動變慢。咱們也能夠
使用線程去加載,但咱們必須保證加載完成以後再進行某個外部類的請求。固然也能夠真
正等到須要某個外部類的時候再進行Dex加載。這根本上取決於Dex文件自己的大小,太
大了能夠預加載,而比較小能夠等到實際須要的時候再加載。咱們暫且把這個加載了外部
Dex的ClassLoader成爲ExternalClassLoader
上面咱們提到了樹形結構和系統中的多個ClassLoader,當咱們加載外部Dex的時候,我
們是否須要指定一個父ClassLoader呢?咱們固然須要一個父ClassLoader,不然咱們ExternalClassLoader連一些基本的Java類都沒有,它根本不可能成功的加載一個Dex。進一
步的,咱們要選擇哪個ClassLoader來做爲咱們的父親呢?是SystemClassLoader仍是
ExternalClassLoader?這是根據狀況來定的,若是外部的Dex文件裏沒有任何和Android
相關的代碼,那麼SystemClassLoader是咱們的首選,不然咱們就應該用ContextClassLoader。若是是後者的狀況,咱們的樹能夠被當作一個鏈表。
3.3 外部的View, Acitivity等
咱們知道,咱們編寫的四大組建都不是由咱們本身來建立的,是由系統來給咱們構造並
管理其生命週期的。那麼這個過程是什麼樣的呢?拿Activity來舉例,咱們須要經過調用
當前Activity/Context的startActivity,傳入一個Intent來調用啓動一個新的Activity。系
統有一個ActivityManager來處理這裏的邏輯。這裏的邏輯至關的複雜,但簡單來講,
ActivityManager會收到並處理這個Intent,從而決定是是啓動一個新的,仍是把舊的放
到前臺。它會先查找這個Activity在哪一個應用裏面,這是經過掃描每一個應用的AndroidManifest來肯定。這些信息是在PackageManager裏面被檢索的。總之若是這個Activity
再也不任何的manifest裏面,它就不可能被啓動。因此僅有一個Activity類是不夠的,咱們
須要在manifest裏面聲明它。
上面是Activity的狀況,Service之類的也是同理。那麼View怎麼辦?儘管咱們能夠直接創
建View,可是大部分的View都不是咱們建立的,而是經過XML佈局文件Inflate出來的。
也就是說,咱們在XML定義了一些外部Dex裏面的View,那麼顯然這個XML是不能被成
功的Inflate的。由於除非系統會使用咱們的ExternalClassLoader,不然它確定是找不到
咱們的類的:ContextClassLoader裏面並無外部Dex中的類。
也就是說問題的根本在於,對於那些Android系統爲咱們建立的對象,它是不能包含在外
部Dex裏面的。而Android系統中大部分的組建類的生命週期都交給了系統來管理。咱們
3
不可能本身來建立這些類對象。那麼另外一種思路:咱們是否是能夠經過使用咱們的ExternalClassLoader來代替ContextClassLoader呢?儘管系統的ContextClassLoader是私有
的,可是咱們能夠經過反射強制的把它替換成咱們的ExternalClassLoader。而對於那些
外部的組建(Activity等),儘管咱們沒有它們的類,可是並不影響咱們在AndroidManifest裏面聲明這個Activity。由於Android系統只是把它做爲一個檢索,並不會真正檢查它
裏面的組建是否是真的在虛擬機環境中已經被加載了,只有真正使用Intent啓動某個組建
的時候纔會去檢查。而只要咱們保證這個時候咱們已經加載了外部的ClassLoader,那麼
這個組建就能夠被正常的啓動。
還有一點,除了咱們要爲外部可能有的組建在AndroidManifest裏面作聲明一外,那些外
部組建可能用到的權限咱們也須要一一聲明,例如若是外部Activity使用了相機功能,那
麼若是咱們的Manifest裏面沒有聲明使用相機功能的權限的話,即使這個Activity能成功
爲加載出來,仍然是不能使用的。
4 核心代碼段
加載外部Dex
mClassLoader = new DexClassLoader ( f . getAbsolutePath ( ) ,
mContext . getCacheDir ( ) . getAbsolutePath ( ) ,
null , mContext . getClassLoader ( ) ) ;
讓系統使用ExternalClassLoader
t r y {
F ie ld mMainThread = ge t F ie ld ( A c t i v i t y . class , 」mMainThread」) ;
Object mainThread = mMainThread . get ( a c t i v i t y ) ;
Class t hreadClass = mainThread . get Class ( ) ;
F ie ld mPackages = ge t F ie ld ( threadClass , 」mPackages」) ;
WeakReference<?> r e f ;
Map< St ring , ?> map =(Map< St ring , ? >) mPackages . get (mainThread ) ;
r e f = (WeakReference<? >) map . get (mContext . getPackageName ( ) ) ;
Object apk = r e f . get ( ) ;
Class apkClass = apk . get Class ( ) ;
F ie ld mClassLoader = ge t F ie ld ( apkClass , 」mClassLoader」) ;
mClassLoader . set (apk , classLoader ) ;
} catch ( I llegalArgument Except ion e) {
i f (DEBUG) {
e . print St ackTrace ( ) ;
}
} catch ( I llega lAc c essEx c ept ion e) {
i f (DEBUG) {
4
e . print St ackTrace ( ) ;
}
}
5android