本文將學(xué)習(xí):類加載器 ClassLoader播玖。
之前學(xué)了 Java 字節(jié)碼文件(.class)的格式椎工。一個(gè)完整的 Java 程序是由多個(gè) .class 文件組成的,在程序運(yùn)行過(guò)程中蜀踏,需要將這些 .class 文件加載到 JVM 中才可以使用维蒙。而負(fù)責(zé)加載這些 .class 文件的就是本文要學(xué)習(xí)的類加載器(ClassLoader)。
Java 中的類何時(shí)被加載器加載
在 Java 程序啟動(dòng)的時(shí)候果覆,并不會(huì)一次性加載程序中所有的 .class 文件颅痊,而是在程序的運(yùn)行過(guò)程中,動(dòng)態(tài)地加載相應(yīng)的類到內(nèi)存中局待。
通常情況下斑响,Java 程序中的 .class 文件會(huì)在以下 2 種情況下被 ClassLoader 主動(dòng)加載到內(nèi)存中:
- 調(diào)用類構(gòu)造器
- 調(diào)用類中的靜態(tài)(static)變量或者靜態(tài)方法
Java 中的 ClassLoader
JVM 中自帶 3 個(gè)類加載器:
- 啟動(dòng)類加載器 BootstrapClassLoader
- 擴(kuò)展類加載器 ExtClassLoader (JDK 1.9 之后,改名為 PlatformClassLoader)
- 系統(tǒng)加載器 APPClassLoader
以上 3 者在 JVM 中有各自分工钳榨,但是又互相有依賴舰罚。
APPClassLoader 系統(tǒng)類加載器
部分源碼如下:
可以看出,AppClassLoader 主要加載系統(tǒng)屬性“java.class.path”配置下類文件薛耻,也就是環(huán)境變量 CLASS_PATH 配置的路徑营罢。因此 AppClassLoader 是面向用戶的類加載器,我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來(lái)加載的饼齿。
ExtClassLoader 擴(kuò)展類加載器
部分源碼如下:
可以看出饲漾,ExtClassLoader 加載系統(tǒng)屬性“java.ext.dirs”配置下類文件,可以打印出這個(gè)屬性來(lái)查看具體有哪些文件:
BootstrapClassLoader 啟動(dòng)類加載器
BootstrapClassLoader 同上面的兩種 ClassLoader 不太一樣缕溉。
首先考传,它并不是使用 Java 代碼實(shí)現(xiàn)的,而是由 C/C++ 語(yǔ)言編寫的证鸥,它本身屬于虛擬機(jī)的一部分僚楞。因此我們無(wú)法在 Java 代碼中直接獲取它的引用。如果嘗試在 Java 層獲取 BootstrapClassLoader 的引用敌土,系統(tǒng)會(huì)返回 null镜硕。
BootstrapClassLoader 加載系統(tǒng)屬性“sun.boot.class.path”配置下類文件运翼,可以打印出這個(gè)屬性來(lái)查看具體有哪些文件:
結(jié)果如下:
可以看到返干,這些全是 JRE 目錄下的 jar 包或者 .class 文件。
雙親委派模式(Parents Delegation Model)
既然 JVM 中已經(jīng)有了這 3 種 ClassLoader血淌,那么 JVM 又是如何知道該使用哪一個(gè)類加載器去加載相應(yīng)的類呢矩欠?答案就是:雙親委派模式财剖。
雙親委派模式
所謂雙親委派模式就是,當(dāng)類加載器收到加載類或資源的請(qǐng)求時(shí)癌淮,通常都是先委托給父類加載器加載躺坟,也就是說(shuō),只有當(dāng)父類加載器找不到指定類或資源時(shí)乳蓄,自身才會(huì)執(zhí)行實(shí)際的類加載過(guò)程咪橙。
其具體實(shí)現(xiàn)代碼是在 ClassLoader.java 中的 loadClass 方法中,如下所示:
解釋說(shuō)明:
- 判斷該 Class 是否已加載虚倒,如果已加載美侦,則直接將該 Class 返回。
- 如果該 Class 沒(méi)有被加載過(guò)魂奥,則判斷 parent 是否為空菠剩,如果不為空則將加載的任務(wù)委托給parent。
- 如果 parent == null耻煤,則直接調(diào)用 BootstrapClassLoader 加載該類具壮。
- 如果 parent 或者 BootstrapClassLoader 都沒(méi)有加載成功,則調(diào)用當(dāng)前 ClassLoader 的 findClass 方法繼續(xù)嘗試加載哈蝇。
那這個(gè) parent 是什么呢棺妓? 我們可以看下 ClassLoader 的構(gòu)造器,如下:
可以看出炮赦,在每一個(gè) ClassLoader 中都有一個(gè) CLassLoader 類型的 parent 引用涧郊,并且在構(gòu)造器中傳入值。如果我們繼續(xù)查看源碼眼五,可以看到 AppClassLoader 傳入的 parent 就是 ExtClassLoader妆艘,而 ExtClassLoader 并沒(méi)有傳入任何 parent,也就是 null看幼。
舉例說(shuō)明
比如執(zhí)行以下代碼:
Test test = new Test();
默認(rèn)情況下批旺,JVM 首先使用 AppClassLoader 去加載 Test 類。
- AppClassLoader 將加載的任務(wù)委派給它的父類加載器(parent)—ExtClassLoader诵姜。
- ExtClassLoader 的 parent 為 null汽煮,所以直接將加載任務(wù)委派給 BootstrapClassLoader。
- BootstrapClassLoader 在 jdk/lib 目錄下無(wú)法找到 Test 類棚唆,因此返回的 Class 為 null暇赤。
- 因?yàn)?parent 和 BootstrapClassLoader 都沒(méi)有成功加載 Test 類,所以AppClassLoader會(huì)調(diào)用自身的 findClass 方法來(lái)加載 Test宵凌。
最終 Test 類就是被 AppClassLoader 加載到內(nèi)存中鞋囊,可以通過(guò)如下代碼印證此結(jié)果:
打印結(jié)果為:
可以看出,Test 的 ClassLoader 為 AppClassLoader 類型瞎惫,而 AppClassLoader 的 parent 為 ExtClassLoader 類型溜腐。ExtClassLoader 的 parent 為 null译株。
注意:“雙親委派”機(jī)制只是 Java 推薦的機(jī)制,并不是強(qiáng)制的機(jī)制挺益。我們可以繼承 java.lang.ClassLoader 類歉糜,實(shí)現(xiàn)自己的類加載器。如果想保持雙親委派模型望众,就應(yīng)該重寫 findClass(name) 方法匪补;如果想破壞雙親委派模型,可以重寫 loadClass(name) 方法烂翰。
自定義 ClassLoader
JVM 中預(yù)置的 3 種 ClassLoader 只能加載特定目錄下的 .class 文件叉袍,如果我們想加載其他特殊位置下的 jar 包或類時(shí)(比如,我要加載網(wǎng)絡(luò)或者磁盤上的一個(gè) .class 文件)刽酱,默認(rèn)的 ClassLoader 就不能滿足我們的需求了喳逛,所以需要定義自己的 Classloader 來(lái)加載特定目錄下的 .class 文件。
自定義 ClassLoader 步驟
- 自定義一個(gè)類繼承抽象類 ClassLoader棵里。
- 重寫 findClass 方法润文。
- 在 findClass 中,調(diào)用 defineClass 方法將字節(jié)碼轉(zhuǎn)換成 Class 對(duì)象殿怜,并返回典蝌。
用一段偽代碼來(lái)描述這段過(guò)程如下:
自定義 ClassLoader 實(shí)踐
首先在本地電腦上創(chuàng)建一個(gè)測(cè)試類 Secret.java,代碼如下:
然后將其放到如下磁盤路徑:
并且把.java文件編譯為.class文件头谜。
接下來(lái)骏掀,創(chuàng)建 DiskClassLoader 繼承 ClassLoader,重寫 findClass 方法柱告,并在其中調(diào)用 defineClass 創(chuàng)建 Class截驮,代碼如下:
最后,寫一個(gè)測(cè)試自定義 DiskClassLoader 的測(cè)試類际度,用來(lái)驗(yàn)證我們自定義的 DiskClassLoader 是否能正常 work葵袭。
解釋說(shuō)明:
① 代表需要?jiǎng)討B(tài)加載的 class 的路徑。
② 代表需要?jiǎng)討B(tài)加載的類名乖菱。
③ 代表需要?jiǎng)討B(tài)調(diào)用的方法名稱坡锡。
最后執(zhí)行上述 testClassLoader 方法,并打印如下結(jié)果窒所,說(shuō)明我們自定義的 DiskClassLoader 可以正常工作鹉勒。
注意:上述動(dòng)態(tài)加載 .class 文件的思路,經(jīng)常被用作熱修復(fù)和插件化開(kāi)發(fā)的框架中吵取,包括 QQ 空間熱修復(fù)方案禽额、微信 Tink 等原理都是由此而來(lái)『Tǎ客戶端只要從服務(wù)端下載一個(gè)加密的 .class 文件绵疲,然后在本地通過(guò)事先定義好的加密方式進(jìn)行解密哲鸳,最后再使用自定義 ClassLoader 動(dòng)態(tài)加載解密后的 .class 文件臣疑,并動(dòng)態(tài)調(diào)用相應(yīng)的方法盔憨。
Android 中的 ClassLoader
本質(zhì)上,Android 和傳統(tǒng)的 JVM 是一樣的讯沈,也需要通過(guò) ClassLoader 將目標(biāo)類加載到內(nèi)存郁岩,類加載器之間也符合雙親委派模型。但是在 Android 中缺狠, ClassLoader 的加載細(xì)節(jié)有略微的差別问慎。
在 Android 虛擬機(jī)里是無(wú)法直接運(yùn)行 .class 文件的,Android 會(huì)將所有的 .class 文件轉(zhuǎn)換成一個(gè) .dex 文件挤茄,并且 Android 將加載 .dex 文件的實(shí)現(xiàn)封裝在 BaseDexClassLoader 中如叼,而我們一般只使用它的兩個(gè)子類:PathClassLoader 和 DexClassLoader。
PathClassLoader
PathClassLoader 用來(lái)加載系統(tǒng) apk 和被安裝到手機(jī)中的 apk 內(nèi)的 dex 文件穷劈。它的 2 個(gè)構(gòu)造函數(shù)如下:
參數(shù)說(shuō)明:
- dexPath:dex 文件路徑笼恰,或者包含 dex 文件的 jar 包路徑;
- librarySearchPath:C/C++ native 庫(kù)的路徑歇终。
PathClassLoader 里面除了這 2 個(gè)構(gòu)造方法以外就沒(méi)有其他的代碼了社证,具體的實(shí)現(xiàn)都是在 BaseDexClassLoader 里面,其 dexPath 比較受限制评凝,一般是已經(jīng)安裝應(yīng)用的 apk 文件路徑追葡。
當(dāng)一個(gè) App 被安裝到手機(jī)后,apk 里面的 class.dex 中的 class 均是通過(guò) PathClassLoader 來(lái)加載的奕短,可以通過(guò)如下代碼驗(yàn)證:
打印結(jié)果如下:
DexClassLoader
先來(lái)看官方對(duì) DexClassLoader 的描述:
A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry.
This can be used to execute code notinstalled as part of an application.
很明顯宜肉,對(duì)比 PathClassLoader 只能加載已經(jīng)安裝應(yīng)用的 dex 或 apk 文件,DexClassLoader 則沒(méi)有此限制翎碑,可以從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件崖飘,這也是插件化和熱修復(fù)的基礎(chǔ),在不需要安裝應(yīng)用的情況下杈女,完成需要使用的 dex 的加載朱浴。
DexClassLoader 的源碼里面只有一個(gè)構(gòu)造方法,代碼如下:
參數(shù)說(shuō)明:
- dexPath:包含 class.dex 的 apk达椰、jar 文件路徑 翰蠢,多個(gè)路徑用文件分隔符(默認(rèn)是“:”)分隔。
- optimizedDirectory:用來(lái)緩存優(yōu)化的 dex 文件的路徑啰劲,即從 apk 或 jar 文件中提取出來(lái)的 dex 文件梁沧。該路徑不可以為空,且應(yīng)該是應(yīng)用私有的蝇裤,有讀寫權(quán)限的路徑廷支。
使用 DexClassLoader 實(shí)現(xiàn)熱修復(fù)
理論知識(shí)都是為實(shí)踐作基礎(chǔ)频鉴,接下來(lái)使用 DexClassLoader 來(lái)模擬熱修復(fù)功能的實(shí)現(xiàn)。
創(chuàng)建 Android 項(xiàng)目 DexClassLoaderHotFix
項(xiàng)目結(jié)構(gòu)如下:
ISay.java 是一個(gè)接口恋拍,內(nèi)部只定義了一個(gè)方法 saySomething垛孔。
SayException.java 實(shí)現(xiàn)了 ISay 接口,但是在 saySomething 方法中施敢,打印“something wrong here”來(lái)模擬一個(gè)線上的 bug周荐。
最后在 MainActivity.java 中,當(dāng)點(diǎn)擊 Button 的時(shí)候僵娃,將 saySomething 返回的內(nèi)容通過(guò) Toast 顯示在屏幕上概作。
最后運(yùn)行效果如下:
創(chuàng)建 HotFix patch 包
新建 Java 項(xiàng)目(創(chuàng)建一個(gè)Java Library Module也可以),項(xiàng)目結(jié)構(gòu)如下:
然后創(chuàng)建兩個(gè)文件 ISay.java 和 SayHotFix.java默怨。
ISay 接口的包名和類名必須和 Android 項(xiàng)目中保持一致讯榕。SayHotFix 實(shí)現(xiàn) ISay 接口,并在 saySomething 中返回了新的結(jié)果匙睹,用來(lái)模擬 bug 修復(fù)后的結(jié)果愚屁。
將 ISay.java 和 SayHotFix.java 打包成 SayHotFixPro.jar,然后通過(guò) dx 工具將生成的 SayHotFixPro.jar 包中的 class 文件優(yōu)化為 dex 文件垃僚,生成hotfix.jar集绰。
打包:
使用Gradle中的jar task完成打包,會(huì)在左側(cè)生成SayHotFixPro.jar包谆棺。如下圖:
dx工具使用:
在libs文件目錄下栽燕,使用cmd:
如果不能使用dx,需要先配置一下dx.bat的環(huán)境變量:
這個(gè)dx.bat一般在如下路徑:
上述 hotfix.jar 就是我們最終需要用作 hotfix 的 jar 包改淑。
將 HotFix patch 包拷貝到 SD 卡主目錄碍岔,并使用 DexClassLoader 加載 SD 卡中的 ISay 接口
首先將 HotFix patch 保存到本地目錄下。一般在真實(shí)項(xiàng)目中朵夏,我們可以通過(guò)向后端發(fā)送請(qǐng)求的方式蔼啦,將最新的 HotFix patch 下載到本地中。這里為了演示,我直接使用 adb 命令將 hotfix.jar 包 push 到 SD 卡的主目錄下:
adb push hotfix.jar /storage/self/primary/
接下來(lái),修改 MainActivity 中的邏輯牺陶,使用 DexClassLoader 加載 HotFix patch 中的 SayHotFix 類潜沦,如下:
注意:因?yàn)樾枰L問(wèn) SD 卡中的文件歹河,所以需要在 AndroidManifest.xml 中申請(qǐng)權(quán)限。在26版本以后需要?jiǎng)討B(tài)獲取,為了方便驗(yàn)證,可以直接手動(dòng)先給應(yīng)用存儲(chǔ)權(quán)限
沒(méi)有權(quán)限會(huì)出現(xiàn)以下錯(cuò)誤:
最后運(yùn)行效果如下:
總結(jié):
- ClassLoader 就是用來(lái)加載 class 文件的辩棒,不管是 jar 中還是 dex 中的 class。
- Java 中的 ClassLoader 通過(guò)雙親委托來(lái)加載各自指定路徑下的 class 文件。
- 可以自定義 ClassLoader一睁,一般覆蓋 findClass() 方法钻弄,不建議重寫 loadClass 方法。
- Android 中常用的兩種 ClassLoader 分別為:PathClassLoader 和 DexClassLoader者吁。