JVM與DVM ——(5)深入理解 ClassLoader 的加載機(jī)制

本文將學(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)存中:

  1. 調(diào)用類構(gòu)造器
  2. 調(diào)用類中的靜態(tài)(static)變量或者靜態(tài)方法

Java 中的 ClassLoader

JVM 中自帶 3 個(gè)類加載器:

  1. 啟動(dòng)類加載器 BootstrapClassLoader
  2. 擴(kuò)展類加載器 ExtClassLoader (JDK 1.9 之后,改名為 PlatformClassLoader)
  3. 系統(tǒng)加載器 APPClassLoader

以上 3 者在 JVM 中有各自分工钳榨,但是又互相有依賴舰罚。

APPClassLoader 系統(tǒng)類加載器

部分源碼如下:

img

可以看出,AppClassLoader 主要加載系統(tǒng)屬性“java.class.path”配置下類文件薛耻,也就是環(huán)境變量 CLASS_PATH 配置的路徑营罢。因此 AppClassLoader 是面向用戶的類加載器,我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來(lái)加載的饼齿。

ExtClassLoader 擴(kuò)展類加載器

部分源碼如下:

img

可以看出饲漾,ExtClassLoader 加載系統(tǒng)屬性“java.ext.dirs”配置下類文件,可以打印出這個(gè)屬性來(lái)查看具體有哪些文件:

img
img

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)查看具體有哪些文件:

img

結(jié)果如下:

img

可以看到返干,這些全是 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 方法中,如下所示:

img

解釋說(shuō)明:

  1. 判斷該 Class 是否已加載虚倒,如果已加載美侦,則直接將該 Class 返回。
  2. 如果該 Class 沒(méi)有被加載過(guò)魂奥,則判斷 parent 是否為空菠剩,如果不為空則將加載的任務(wù)委托給parent。
  3. 如果 parent == null耻煤,則直接調(diào)用 BootstrapClassLoader 加載該類具壮。
  4. 如果 parent 或者 BootstrapClassLoader 都沒(méi)有加載成功,則調(diào)用當(dāng)前 ClassLoader 的 findClass 方法繼續(xù)嘗試加載哈蝇。

那這個(gè) parent 是什么呢棺妓? 我們可以看下 ClassLoader 的構(gòu)造器,如下:

img

可以看出炮赦,在每一個(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 類。

  1. AppClassLoader 將加載的任務(wù)委派給它的父類加載器(parent)—ExtClassLoader诵姜。
  2. ExtClassLoader 的 parent 為 null汽煮,所以直接將加載任務(wù)委派給 BootstrapClassLoader。
  3. BootstrapClassLoader 在 jdk/lib 目錄下無(wú)法找到 Test 類棚唆,因此返回的 Class 為 null暇赤。
  4. 因?yàn)?parent 和 BootstrapClassLoader 都沒(méi)有成功加載 Test 類,所以AppClassLoader會(huì)調(diào)用自身的 findClass 方法來(lái)加載 Test宵凌。

最終 Test 類就是被 AppClassLoader 加載到內(nèi)存中鞋囊,可以通過(guò)如下代碼印證此結(jié)果:

img

打印結(jié)果為:

img

可以看出,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 步驟

  1. 自定義一個(gè)類繼承抽象類 ClassLoader棵里。
  2. 重寫 findClass 方法润文。
  3. 在 findClass 中,調(diào)用 defineClass 方法將字節(jié)碼轉(zhuǎn)換成 Class 對(duì)象殿怜,并返回典蝌。

用一段偽代碼來(lái)描述這段過(guò)程如下:

img

自定義 ClassLoader 實(shí)踐

首先在本地電腦上創(chuàng)建一個(gè)測(cè)試類 Secret.java,代碼如下:

image-20201229160237450.png

然后將其放到如下磁盤路徑:

image-20201229160346208.png

并且把.java文件編譯為.class文件头谜。

接下來(lái)骏掀,創(chuàng)建 DiskClassLoader 繼承 ClassLoader,重寫 findClass 方法柱告,并在其中調(diào)用 defineClass 創(chuàng)建 Class截驮,代碼如下:

image-20201229160515322.png

最后,寫一個(gè)測(cè)試自定義 DiskClassLoader 的測(cè)試類际度,用來(lái)驗(yàn)證我們自定義的 DiskClassLoader 是否能正常 work葵袭。

image-20201229161123239.png

解釋說(shuō)明:

① 代表需要?jiǎng)討B(tài)加載的 class 的路徑。

② 代表需要?jiǎng)討B(tài)加載的類名乖菱。

③ 代表需要?jiǎng)討B(tài)調(diào)用的方法名稱坡锡。

最后執(zhí)行上述 testClassLoader 方法,并打印如下結(jié)果窒所,說(shuō)明我們自定義的 DiskClassLoader 可以正常工作鹉勒。

image-20201229161254895.png

注意:上述動(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ù)如下:

img

參數(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)證:

image-20201229162655614.png

打印結(jié)果如下:

image-20201229162603477.png
image-20201229162748769.png

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)造方法,代碼如下:

img

參數(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)如下:

image-20201229163937525.png

ISay.java 是一個(gè)接口恋拍,內(nèi)部只定義了一個(gè)方法 saySomething垛孔。

image-20201229164713482.png

SayException.java 實(shí)現(xiàn)了 ISay 接口,但是在 saySomething 方法中施敢,打印“something wrong here”來(lái)模擬一個(gè)線上的 bug周荐。

image-20201229164728971.png

最后在 MainActivity.java 中,當(dāng)點(diǎn)擊 Button 的時(shí)候僵娃,將 saySomething 返回的內(nèi)容通過(guò) Toast 顯示在屏幕上概作。

image-20201229164657325.png

最后運(yùn)行效果如下:

image-20201229164620190.png

創(chuàng)建 HotFix patch 包

新建 Java 項(xiàng)目(創(chuàng)建一個(gè)Java Library Module也可以),項(xiàng)目結(jié)構(gòu)如下:

image-20201230110130705.png

然后創(chuàng)建兩個(gè)文件 ISay.java 和 SayHotFix.java默怨。

image-20201230110252278.png
image-20201230110259782.png

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包谆棺。如下圖:

image-20201230110445896.png

dx工具使用:

在libs文件目錄下栽燕,使用cmd:

image-20201230110945628.png

如果不能使用dx,需要先配置一下dx.bat的環(huán)境變量:

image-20201230111043044.png
image-20201230111108538.png

這個(gè)dx.bat一般在如下路徑:

image-20201230111218518.png

上述 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/
image-20201230111452389.png

接下來(lái),修改 MainActivity 中的邏輯牺陶,使用 DexClassLoader 加載 HotFix patch 中的 SayHotFix 類潜沦,如下:

image-20201230111610071.png

注意:因?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ò)誤:

image-20201230111843674.png

最后運(yùn)行效果如下:

image-20201230105945765.png

總結(jié):

  1. ClassLoader 就是用來(lái)加載 class 文件的辩棒,不管是 jar 中還是 dex 中的 class。
  2. Java 中的 ClassLoader 通過(guò)雙親委托來(lái)加載各自指定路徑下的 class 文件。
  3. 可以自定義 ClassLoader一睁,一般覆蓋 findClass() 方法钻弄,不建議重寫 loadClass 方法。
  4. Android 中常用的兩種 ClassLoader 分別為:PathClassLoader 和 DexClassLoader者吁。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窘俺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子砚偶,更是在濱河造成了極大的恐慌批销,老刑警劉巖洒闸,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件染坯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丘逸,警方通過(guò)查閱死者的電腦和手機(jī)单鹿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)深纲,“玉大人仲锄,你說(shuō)我怎么就攤上這事∨热担” “怎么了儒喊?”我有些...
    開(kāi)封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)币呵。 經(jīng)常有香客問(wèn)我怀愧,道長(zhǎng),這世上最難降的妖魔是什么余赢? 我笑而不...
    開(kāi)封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任芯义,我火速辦了婚禮,結(jié)果婚禮上妻柒,老公的妹妹穿的比我還像新娘扛拨。我一直安慰自己,他們只是感情好举塔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布绑警。 她就那樣靜靜地躺著,像睡著了一般央渣。 火紅的嫁衣襯著肌膚如雪计盒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天痹屹,我揣著相機(jī)與錄音章郁,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛暖庄,可吹牛的內(nèi)容都是我干的聊替。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼培廓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惹悄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起肩钠,我...
    開(kāi)封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泣港,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后价匠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體当纱,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年踩窖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坡氯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洋腮,死狀恐怖箫柳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情啥供,我是刑警寧澤悯恍,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站伙狐,受9級(jí)特大地震影響涮毫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鳞骤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一窒百、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豫尽,春花似錦篙梢、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至榴嗅,卻和暖如春妄呕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嗽测。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工绪励, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肿孵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓疏魏,卻偏偏與公主長(zhǎng)得像停做,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子大莫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

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