1.ClassLoader
一個(gè)完整的Java程序是由多個(gè).class文件組成的,在程序運(yùn)行過程中,需要將這些.class文件加載到JVM中才可以使用。而負(fù)責(zé)加載這些.class文件的就是本類加載器(ClassLoader)。
在 Java 程序啟動(dòng)的時(shí)候,并不會(huì)一次性加載程序中所有的 .class 文件,而是在程序的運(yùn)行過程中,動(dòng)態(tài)地加載相應(yīng)的類到內(nèi)存中。
Java 中的類 被 ClassLoader 加載時(shí)機(jī)
- 調(diào)用類的構(gòu)造器
- 調(diào)用類中的靜態(tài)(static)變量或者靜態(tài)方法
JVM 中自帶 3 個(gè)類加載器
- 啟動(dòng)類加載器
BootstrapClassLoader - 擴(kuò)展類加載器
ExtClassLoader(JDK1.9之后,改名為PlatformClassLoder) - 系統(tǒng)加載器
APPClassLoader
APPClassLoader
應(yīng)用程序類加載器,主要加載系統(tǒng)屬性“java.class.path”配置下類文件,也就是環(huán)境變量CLASS_PATH配置的路徑。因此 AppClassLoader 是面向用戶的類加載器,我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來加載的。
ExtClassLoader
擴(kuò)展類加載器 加載系統(tǒng)屬性“java.ext.dirs”配置下類文件,可以打印出這個(gè)屬性來查看具體有哪些文件:
System.out.println(System.getProperty("java.ext.dirs"));
結(jié)果如下:

BootstrapClassLoader
啟動(dòng)類加載器,是由 C/C++ 語言實(shí)現(xiàn)(其他的類加載器都由Java語言實(shí)現(xiàn)),是虛擬機(jī)自身的一部分,因此我們無法在 Java 代碼中直接獲取它的引用。如果嘗試在 Java 層獲取 BootstrapClassLoader 的引用,系統(tǒng)會(huì)返回 null。
BootstrapClassLoader 加載系統(tǒng)屬性“sun.boot.class.path”配置下類文件,可以打印出這個(gè)屬性來查看具體有哪些文件:
System.out.println(System.getProperty("sun.boot.class.path"));
結(jié)果如下:

2.雙親委派模型
雙親委派模型(Parents Delegation Model),是指當(dāng)類加載器收到加載類或資源的請(qǐng)求時(shí),通常都是先委托給父類加載器加載,只有當(dāng)父類加載器找不到指定類或資源時(shí),自身才會(huì)執(zhí)行實(shí)際的類加載過程。類加載器雙親委派模型如下圖:

雙親委派模型要求除了頂層的啟動(dòng)類加載器以外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里的類加載器之間的父子關(guān)系不會(huì)以繼承(Inheritance)的關(guān)系來實(shí)現(xiàn),而是以組合(Composition)的關(guān)系來復(fù)用父加載器的代碼。這樣做的意義是為了性能,每次加載都會(huì)消耗時(shí)間,但如果父加載器加載過,就可以直接拿來用。
其具體實(shí)現(xiàn)代碼是在 ClassLoader.java 中的 loadClass 方法中,如下所示:

該方法執(zhí)行如下操作:
- 判斷該 Class 是否已加載,如果已加載,則直接將該 Class 返回。
- 如果該 Class 沒有被加載過,則判斷 parent 是否為空,如果不為空則將加載的任務(wù)委托給parent。
- 如果 parent == null,則直接調(diào)用 BootstrapClassLoader 加載該類。
- 如果 parent 或者 BootstrapClassLoader 都沒有加載成功,則調(diào)用當(dāng)前 ClassLoader 的 findClass 方法繼續(xù)嘗試加載。
上面操作中的 parent 是 ClassLoader 的構(gòu)造器中傳入的一個(gè) CLassLoader 類型的 parent 引用,如果我們繼續(xù)查看源碼,可以看到AppClassLoader 傳入的 parent 就是 ExtClassLoader,而 ExtClassLoader 傳入的parent為null。
“雙親委派”機(jī)制只是Java推薦的機(jī)制,并不是強(qiáng)制的機(jī)制。我們可以繼承java.lang.ClassLoader類,實(shí)現(xiàn)自己的類加載器。如果想保持雙親委派模型,就應(yīng)該重寫 findClass(name) 方法;如果想破壞雙親委派模型,可以重寫 loadClass(name) 方法。
3.自定義 ClassLoader
JVM中預(yù)置的3種ClassLoader只能加載特定目錄下的.class文件,如果我們想加載其他特殊位置下的jar包或類時(shí)(比如,我要加載網(wǎng)絡(luò)或者磁盤上的一個(gè).class文件),默認(rèn)的 ClassLoader 就不能滿足我們的需求了,所以需要定義自己的 Classloader 來加載特定目錄下的 .class 文件。
步驟:
- 自定義一個(gè)類繼承抽象類 ClassLoader。
- 重寫 findClass 方法。
- 在 findClass 中,調(diào)用 defineClass 方法將字節(jié)碼轉(zhuǎn)換成 Class 對(duì)象,并返回。
上述動(dòng)態(tài)加載.class文件的思路,經(jīng)常被用作熱修復(fù)和插件化開發(fā)的框架中,包括QQ空間熱修復(fù)方案、微信Tinker等原理都是由此而來??蛻舳酥灰獜姆?wù)端下載一個(gè)加密的.class文件,然后然后在本地通過事先定義好的加密方式進(jìn)行解密,最后再使用自定義 ClassLoader 動(dòng)態(tài)加載解密后的 .class 文件,并動(dòng)態(tài)調(diào)用相應(yīng)的方法。
4.Android 中的 ClassLoader
在Android虛擬機(jī)里是無法直接運(yùn)行.class文件的,Android會(huì)將所有的.class文件轉(zhuǎn)換成一個(gè).dex文件,并且Android將加載.dex文件的實(shí)現(xiàn)封裝在BaseDexClassLoader 中,而我們一般只使用它的兩個(gè)子類:PathClassLoader 和 DexClassLoader。
PathClassLoader
PathClassLoader 用來加載系統(tǒng) apk 和被安裝到手機(jī)中的 apk 內(nèi)的 dex 文件。它的 2 個(gè)構(gòu)造函數(shù)如下:

參數(shù)說明:
- dexPath:dex 文件路徑,或者包含 dex 文件的 jar 包路徑;
- librarySearchPath:C/C++ native 庫的路徑。
PathClassLoader里面除了這2個(gè)構(gòu)造方法以外就沒有其他的代碼了,具體的實(shí)現(xiàn)都是在BaseDexClassLoader里面,其dexPath比較受限制,一般是已經(jīng)安裝應(yīng)用的 apk 文件路徑。
當(dāng)一個(gè) App 被安裝到手機(jī)后,apk 里面的 class.dex 中的 class 均是通過 PathClassLoader 來加載的。
DexClassLoader
對(duì)比PathClassLoader只能加載已經(jīng)安裝應(yīng)用的dex或apk文件,DexClassLoader則沒有此限制,可以從SD卡上加載包含class.dex的.jar 和 .apk 文件,這也是插件化和熱修復(fù)的基礎(chǔ),在不需要安裝應(yīng)用的情況下,完成需要使用的 dex 的加載。
DexClassLoader 的源碼里面只有一個(gè)構(gòu)造方法,代碼如下:

參數(shù)說明:
- dexPath:包含 class.dex 的 apk、jar 文件路徑 ,多個(gè)路徑用文件分隔符(默認(rèn)是“:”)分隔。
- optimizedDirectory:用來緩存優(yōu)化的 dex 文件的路徑,即從 apk 或 jar 文件中提取出來的 dex 文件。該路徑不可以為空,且應(yīng)該是應(yīng)用私有的,有讀寫權(quán)限的路徑。
對(duì)于APP而言,Apk文件中有一個(gè)class.dex文件,這個(gè)dex就是Apk的主dex,是通過PathClassLoader加載的。在App的Activity中,通過getClassLoader方法獲取到的是PathClassLoader,它的父類是BootClassLoader。
對(duì)于插件化而言,有一種方案是將App的ClassLoader替換為自定義的ClassLoader,這樣就要求自定義的ClassLoader模擬雙親委派模型。比較典型的代碼就是Zeus插件化框架。
5.案例:使用 DexClassLoader 加載外部dex
加載外部dex主要流程如下:
- 從服務(wù)器下載插件apk到手機(jī)SDCard
- 讀取插件apk中的dex,生成對(duì)應(yīng)的DexClassLoader
- 使用DexClassLoader的loadClass方法讀取插件dex中的類
我們這里的demo直接把插件apk放到主App的assets目錄中,App啟動(dòng)后,再把a(bǔ)ssets目錄中的插件apk復(fù)制到內(nèi)存,通過這種方式來模擬從服務(wù)器下載插件。
放進(jìn)主App的assets目錄中的 app-debug.apk 是一個(gè)插件,里面有一個(gè)Bean類
public class Bean {
private String name = "JokerWan";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在主App里面加載 app-debug.apk
- 把a(bǔ)ssets目錄下的 app-debug.apk 插件復(fù)制到 /data/data/files 目錄下,這部分邏輯封裝到Utils里面完成
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
- 在App啟動(dòng)的時(shí)候調(diào)用這個(gè)方法,在這個(gè)demo中,我重寫了MainActivity的attachBaseContext方法,在里面做這個(gè)事情
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
Utils.extractAssets(newBase, apkName);
} catch (Throwable e) {
e.printStackTrace();
}
}
- 加載插件 app-debug.apk 中的 dex 生成 DexClassLoader
File extractFile = this.getFileStreamPath(apkName);
dexpath = extractFile.getPath();
fileRelease = getDir("dex", Context.MODE_PRIVATE);
classLoader = new DexClassLoader(dexpath,
fileRelease.getAbsolutePath(), null, getClassLoader());
- 調(diào)用生成的classLoader的loadClass方法加載app-debug.apk 中的Bean類,并調(diào)用其getName方法
Class loadClassBean;
try {
loadClassBean = classLoader.loadClass("com.jokerwan.plugin.Bean");
Object beanObject = loadClassBean.newInstance();
Method getNameMethod = loadClassBean.getMethod("getName");
getNameMethod.setAccessible(true);
String name = (String) getNameMethod.invoke(beanObject);
Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("JokerWan", "msg:" + e.getMessage());
}
雖然拿到了這個(gè)Bean類,但是因?yàn)橹鰽pp中并沒有這個(gè)Bean類,所以我們只能用反射來實(shí)例化Bean并調(diào)用它的getName方法。此時(shí),我們就成功的加載了外部插件apk,并可以獲取到該插件的所有類。
6.總結(jié)
- ClassLoader就是用來加載class文件的,不管是jar中還是dex中的class。
- Java 中的 ClassLoader 通過雙親委派模型來加載各自指定路徑下的 class 文件。
- 可以自定義 ClassLoader,一般覆蓋 findClass() 方法,不建議重寫 loadClass 方法。
- Android 中常用的兩種 ClassLoader 分別為:PathClassLoader 和 DexClassLoader。PathClassLoader 用來加載系統(tǒng) apk 和被安裝到手機(jī)中的 apk 內(nèi)的 dex 文件;DexClassLoader可以從SD卡上加載包含class.dex的.jar 和 .apk 文件。