曾幾何時,國内各大公司掀起了一股研究android動态加載的技術,兩年多過去了,動态加載技術俨然成了android開發中必須掌握的技術。那麼動态加載技術是什麼呢,這裡談談我的個人看法,如有雷同,純屬偶然。
對于動态加載的概念,沒有一個權威的定義,參考網上的解釋,我們舉一個例子,動态加載代碼就是通過在運作時加載外部代碼(磁盤,網絡等)改變程式行為的技術(感覺有點像裝飾者模式)。主要目的是為了達到讓使用者不用重新安裝apk就能更新應用的功能。
為了加深大家對這種概念的了解,我們結合pc端來說說手機端的動态加載。
傳統的pc端動态加載
熟悉java的同學應該比較清楚,java的可執行檔案是jar,運作在虛拟機上jvm上,虛拟機通過classloader加載jar檔案并執行裡面的代碼。是以java程式也可以通過動态調用jar檔案達到動态加載的目的。
動态加載技術在pc軟體領域廣泛使用,比如qq上線的時候忘了某個功能的修複,這個時候就可以用動态加載來修複我們的bug。
android應用的動态加載技術
android應用類似于java程式,隻不過虛拟機換成了dalvik/art,而jar換成了dex。我們知道,在android的apk檔案中往往有一個或者多個dex檔案,系統的類加載器(pathdexclassloader)加載的就是dex檔案,雖然一個apk一旦建構出來,我們是無法更換裡面的dex檔案的,但是我們可以在類加載動态加載外部的dex檔案來達到動态加載的目的。
jvm 的類加載機制是雙親委派模型,這裡貼上jvm加載的圖解。
對于上面這張圖,我們有以下幾點需要說明。
<code>bootstrapclassloader</code>是頂級的類加載器,它是唯一一個不繼承自<code>classloader</code>中的類加載器,它高度內建于 jvm是<code>extensionclassloader</code>的父加載器,它的類加載路徑是<code>jdk\jre\lib</code> 和 使用者指定的虛拟機參數<code>-xbootclasspath</code>的值。
<code>extensionclassloader</code> 是 <code>bootstrapclassloader</code> 的子加載器,同時是 <code>systemclassloader</code>(有的地方稱 <code>appclassloader</code>)的父加載器,它的類加載路徑是 <code>jdk\jre\lib\ext</code> 和系統屬性 <code>java.ext.dirs</code> 的值。
<code>systemclassloader</code> 是 <code>extensionclassloader</code> 的子加載器,同時是我們的應用程式的類加載器,我們在應用程式中編寫的類一般情況下(如果沒有到動态加載技術的話)都是通過這個類加載加載的。它的類加載路徑是環境變量 <code>classpath</code> 的值或者使用者通過指令行可選項 <code>-cp (-classpath)</code> 指定的值。
類加載器由于父子關系形成樹形結構,開發人員可以開發自己的類加載器進而實作動态加載功能,但必須給這個類加載器指定樹上的一個節點作為它的父加載器。
因為類加載器是通過包名和類名(或者說類的全限定名),是以由于委派式加載機制的存在,全限定名相同的類不會在有 祖先—子孫 關系的類加載器上分别加載一次,不管這兩個類的實作是否一樣。
不同的類加載器加載的類一定是不同的類,即使它們的全限定名一樣。如果全限定名一樣,那麼根據上一條,這兩個類加載器一定沒有 祖先-子孫 的關系。這樣來看,可以通過自定義類加載器使得相同全限定名但實作不同的類存在于同一 jvm 中,也就是說,類加載器相當于給類在包名之上又加了個命名空間。
如果兩個相同全限定名的類由兩個非 祖先-子孫 關系的類加載器加載,這兩個類之間通過<code>instanceof</code> 和 <code>equals()</code> 等進行比較時總是傳回<code>false</code>。
安卓應用和普通的 java 應用不同,它們運作于 dalvik 虛拟機。jvm 是基于棧的虛拟機,而 dalvik 是基于寄存器的虛拟機。android采用 dex 作為儲存類位元組碼資訊的檔案。當 java 程式編譯成 class 後,編譯器會使用 dx 工具将所有的class 檔案整合到一個 dex 檔案,目的是使其中各個類能夠共享資料,在一定程度上降低了備援,同時也是檔案結構更加緊湊。
為了說明android的類加載機制,我們需要對android的classloader做一個了解。
安卓中兩個重要的類加載器:dexclassloader 和 pathclassloader。
那麼對于android來說,我們來看看android的加載模型。
我們首先看一些這兩個類。
<a target="_blank" href="http://androidxref.com/7.0.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/dexclassloader.java">dexcloassloader</a>
<a target="_blank" href="http://androidxref.com/7.0.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/pathclassloader.java">pathclassloader</a>
可以看到,這兩個類加載器都是繼承自 basedexclassloader,隻是分别實作了自己的構造方法。
<a target="_blank" href="http://androidxref.com/7.0.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/pathclassloader.java">asedexclassloader</a>
我們發現basedexclassloader作為一個基類,其構造極其簡單,它做了兩件事:連接配接了父加載器;構造了一個 dexpathlist 執行個體儲存在 pathlist 中。
參數意思如下:
第一個參數指的是我們要加載的 dex 檔案的路徑,它有可能是多個 dex 路徑,取決于我們要加載的 dex 檔案的個數,多個路徑之間用 <code>:</code> 隔開。
第二個參數指的是優化後的 dex 存放目錄。實際上,dex 其實還并不能被虛拟機直接加載,它需要系統的優化工具優化後才能真正被利用。優化之後的 dex 檔案我們把它叫做 odex (optimized dex,說明這是被優化後的 dex)檔案。其實從 class 到 dex 也算是經曆了一次優化,這種優化的是機器無關的優化,也就是說不管将來運作在什麼機器上,這種優化都是遵循固定模式的,是以這種優化發生在 apk 編譯。而從 dex 檔案到 odex 檔案,是機器相關的優化,它使得 odex 适配于特定的硬體環境,不同機器這一步的優化可能有所不同,是以這一步需要在應用安裝等運作時期由機器來完成。需要注意的是,在較早版本的系統中,這個目錄可以指定為外部存儲中的目錄,較新版本的系統為了安全隻允許其為應用程式私有存儲空間(<code>/data/data/apk-package-name/</code>)下的目錄,一般我們可以通過 <code>context#getdir(string dirname)</code> 得到這個目錄。
第三個參數的意義是庫檔案的的搜尋路徑,一般來說是 <code>.so</code> 庫檔案的路徑,也可以指明多個路徑。
第四個參數就是要傳入的父加載器,一般情況我們可以通過 <code>context#getclassloader()</code> 得到應用程式的類加載器然後把它傳進去。
好了,到這裡就很清楚了,dalvik 虛拟機要加載的 dex 檔案的路徑(dexpathlist),那麼dalvik是如何找到dex的呢?有人會說反射,對,大方向對了。那麼我們看看系統究竟是怎麼做的。
dexpathlist
這裡我們主要看如下幾行代碼:
這段代碼主要是給 dexelements和nativelibrarypathelements指派。我們知道android在通過預設的虛拟機dex後,會繼續優化為odex 檔案。
dexelements 是通過 makedexelements() 方法得到的。makedexelements的方法裡面我們主要關注前面兩個參數,我們來看一下splitdexpath(dexpath)。
這個方法很簡單就是用,分隔的路徑分割後儲存為 file 類型的清單傳回。現在看看 <code>makedexelements()</code> 這個方法:
通過代碼我們可以大緻了解到,這個方法就是将之前的file對象通過重新組合成一個新的elements對象,然後我們loader讀取的就是element對象。看一下 loaddexfile() 怎樣加載 dexfile 的
先說明下無論是 <code>dexfile(file file, classloader loader, elements[] elements)</code> 還是 dexfile.loaddex() 最終都會調用 <code>dexfile(string sourcename, string outputname, int flags, classloader loader, dexpathlist.element[] elements)</code> 這個構造方法。是以這個方法的邏輯就是:如果 <code>optimizeddirectory</code> 為 null,那麼就直接利用 file 的路徑構造一個 <code>dexfile</code>;否則就根據要加載的 dex(或者包含了 dex 的 zip) 的檔案名和優化後的 dex 存放的目錄組合成優化後的 dex(也就是 odex)檔案的輸出路徑,然後利用原始路徑和優化後的輸出路徑構造出一個<code>dexfile.</code>
<code>分析完這兩字段,現在我們回過頭來看看 dexpathlist 這個對象,這個對象持有 dexelements 和 nativelibrarypathelements 這兩個屬性,也就是說它儲存了 dex 和 本地方法庫。</code>
為了加深大家對dexpathlist的了解,我們來看看官方的說明。
a pair of lists of entries, associated with a {@code classloader}. one of the lists is a dex/resource path — typically referred to as a “class path” — list, and the other names directories containing native code libraries. class path entries may be any of: a {@code .jar} or {@code .zip} file containing an optional top-level {@code classes.dex} file as well as arbitrary resources, or a plain {@code .dex} file (with no possibility of associated resources).</br>this class also contains methods to use these lists to look up classes and resources.
大概的意思就是 dexpathlist 的作用和 jvm 中的 classpath 的作用類似,jvm 根據 classpath 來查找類,而 dalvik 利用 dexpathlist 來查找并加載類。dexpathlist 包含的路徑可以是 .dex 檔案的路徑,也可以是包含了 dex 的 .jar 和 .zip 檔案的路徑。
我們知道,一個類加載器的入口方法是 <code>loadclass()。這是java語音所共有的。類加載器通過findclass()找到所需要加載的類。</code>
<code></code>
basedexclassloader 也繼承自 classloader,是以我們就從 findclass() 方法來分析下 baseclassloader 加載類的過程。
這個方法極其簡單,主要風格findclass找到類 <code>class c = pathlist.findclass(name, suppressedexception)</code>這裡<code>baseclassloader</code> 把查找類的任務委托給了 <code>pathlist</code>。那麼我們來看一下android的dexpathlist的findclass又做了什麼事情。
它周遊了 <code>dexelements</code> 中的所有 <code>dexfile</code>,通過 <code>dexfile</code> 的<code>loadclassbinaryname()</code> 方法加載目标類。<code>dexelements</code> 又把查找類的任務委托給了<code>dexfile</code>
到這裡我們就已經很明白了,opendexfile調用opendexfilenative()方法,(
它做的事就是把對應的 dex 檔案加載到記憶體中,然後傳回給 java 層一個類似句柄一樣的東西 <code>object:mcookie。</code>
在構造方法中 <code>dexfile</code> 就完成了 dex 檔案的加載過程。現在我們回到 <code>dexfile</code> 對象的<code>loadclassbinaryname()</code>:
看到這裡我們明白了,class 對象在 java 層加載過程的盡頭就是這個 <code>defineclass()</code> 方法,這個方法調用本地法 <code>defineclassnative()</code> 從 dex 中查找目标類,如果找到了,就把這個代表這個類的 <code>class</code> 對象傳回。到此,android的加載過程我們終于看完了。
到這裡我們回頭看看android的兩個類加載器:<code>dexclassloader()</code> 和 <code>pathclassloader()。</code>
<code>dexclassloader</code> 用來加載 .dex 檔案以及包含 dex 檔案的 .jar、.zip 和未安裝的 .apk 檔案,是以需要指定優化後的 dex 檔案的輸出路徑;
<code>pathclassloader</code> 一般用來加載已經安裝到裝置上的<code>.apk</code>,因為應用在安裝的時候已經對 apk 檔案中的 dex 進行了優化,并且會輸出到 <code>/data/dalvik-cache</code> 目錄下(android m 在這目錄下找不到,應該是改成了 <code>/data/app/com.example.app-x/oat</code> 目錄下),是以它不需要指定優化後 dex 的輸出路徑。