深入淺出ClassLoader加載機(jī)制

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ī)

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

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

  1. 啟動(dòng)類加載器BootstrapClassLoader
  2. 擴(kuò)展類加載器ExtClassLoader(JDK1.9之后,改名為PlatformClassLoder
  3. 系統(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í)行如下操作:

  1. 判斷該 Class 是否已加載,如果已加載,則直接將該 Class 返回。
  2. 如果該 Class 沒有被加載過,則判斷 parent 是否為空,如果不為空則將加載的任務(wù)委托給parent。
  3. 如果 parent == null,則直接調(diào)用 BootstrapClassLoader 加載該類。
  4. 如果 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 文件。

步驟:

  1. 自定義一個(gè)類繼承抽象類 ClassLoader。
  2. 重寫 findClass 方法。
  3. 在 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主要流程如下:

  1. 從服務(wù)器下載插件apk到手機(jī)SDCard
  2. 讀取插件apk中的dex,生成對(duì)應(yīng)的DexClassLoader
  3. 使用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

  1. 把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);
        }

    }
  1. 在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();
        }
    }
  1. 加載插件 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());
  1. 調(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 文件。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容