Android 簡單熱修復(fù)(上)——Java類加載器

作為陽歷新年的第一篇文章,本想把之前總結(jié)的用到實踐中,簡單寫了個鐘表,寫著寫著感覺索然無味(/ □ )。寫完后,百無聊賴之際,隨便翻看了些技術(shù)文章。讓我眼前為之一亮的有兩個:

  • Android 破解跳一跳
  • Android 簡單熱修復(fù)原理

作為Android狗的我果斷選擇了熱修復(fù)的介紹,在看完Android類加載器的源碼后,對于簡單的熱修復(fù)原理算是了解了一些。遂作此文,以謹記。



在介紹Android熱修復(fù)原理之前,有必要了解下關(guān)于Java的類加載器的相關(guān)知識。在《深入理解Java虛擬機》一書中關(guān)于類加載的可以分為五個過程:

  1. 加載
    在加載過程中需要完成3件事情:
    1.1 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
    1.2 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
    1.3 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
  2. 驗證
    這一階段的主要目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
  3. 準備
    準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段 ,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。
  4. 解析
    解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。
  5. 初始化
    初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。

關(guān)于詳細介紹,還是乖乖看書吧。
OK,知道了類加載的過程,但是究竟是什么“東西”加載類呢?答案是類加載器(ClassLoader),也是今天的主題。
簡單說下類加載器的分類:

  • 啟動類加載器(BootStrap ClassLoader):啟動類加載器負責(zé)將<JAVA_HOME>\lib目錄下中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且被虛擬機識別的類庫加載到虛擬機內(nèi)存中(有點拗口)。通過System.getProperty("sun.boot.class.path")可知默認情況加載如下類庫:
C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_131\jre\classes
  • 擴展類加載器(Extension ClassLoader):擴展類加載器用于將<JAVA_HOME>\lib\ext中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。擴展類加載器加載的類庫(默認情況),可以看到其就是<JAVA_HOME>\lib\ext中的類庫:


    擴展類加載器加載的類庫
  • 應(yīng)用程序類加載器(Application ClassLoader):用于加載用戶類路徑上所指定的類庫,如果程序沒有自定義過自己的類加載器,一般情況下這個就是這個程序的默認類加載器。應(yīng)用程序類加載器加載的類庫(默認情況),可以看到其加載的類庫包括了<JAVA_HOME>\lib和<JAVA_HOME>\lib\ext目錄下的類庫,也就是說如果前兩個沒有找到要加載的類,也可以通過AppClassLoader去加載:


    應(yīng)用程序類加載器加載的類庫

啟動類加載器

上面已經(jīng)說過啟動類加載器會加載的類庫,下午我和一個大佬討論了下關(guān)于java類是否按需加載。答案是:java類是按需加載,只有當需要用到這個類的時候才會加載這個類。在運行時添加-verbose:class參數(shù),我們先看到被加載到內(nèi)存中的類:

啟動時加載的類

啟動類加載了rt.jar中的類,我們可以通過反向來證明某個類是由啟動類加載器加載:

System.out.println(String.class.getClassLoader());

在上面我們只是輸出了一下String這個類的類加載器,結(jié)果如下:

String類加載器

我們可以知道其類加載器是null,這又是為什么呢?我們看下getClassLoader()這個方法的注釋:
注釋

從注釋中我們可以知道如果返回值為null,那么代表此時的類加載器是BootStrap ClassLoader,所以上面所講述的完全沒毛病。

擴展類加載器

先看下默認的<JAVA_HOME>\lib\ext路徑下的類庫有什么:


ext類庫

默認的路徑下加載的類庫并不是特別多,我們挑選其中的一個來測試下:

System.out.println(JarFileSystemProvider.class.getClassLoader());

測試擴展類加載器

從結(jié)果中我們可以知道加載擴展類的加載器是sun.misc.Launcher類的內(nèi)部類ExtClassLoader。

應(yīng)用程序類加載器

應(yīng)用程序加載器用于加載當前程序的類庫(默認情況下),按照上面的測試我們同樣測試下:

// UserModel為當前程序里的一個類
System.out.println(UserModel.class.getClassLoader());

運行結(jié)果:

應(yīng)用程序類加載器

從結(jié)果中我們可以知道加載擴展類的加載器是sun.misc.Launcher類的內(nèi)部類AppClassLoader。
類關(guān)系圖:
類關(guān)系圖

講下每個類加載器的父親:

  • BootStrap ClassLoader:無父類加載器
  • ExtClassLoader:父類加載器BootStrap ClassLoader
  • AppClassLoader:父類加載器ExtClassLoader

關(guān)于三個類加載器的創(chuàng)建

BootStrap ClassLoader

Bootstrap ClassLoader是由C/C++編寫的,它本身是虛擬機的一部分,所以它并不是一個JAVA類,也就是無法在Java代碼中獲取它的引用。

ExtClassLoader的創(chuàng)建

話不多少,還是先看下代碼吧:

Launcher.java:
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 獲得ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        // 將ExtClassLoader作為參數(shù)傳入AppClassLoader中
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    ......

}

static class ExtClassLoader extends URLClassLoader {
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        // 獲取了Ext的目錄
        final File[] var0 = getExtDirs();

        try {
            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                public Launcher.ExtClassLoader run() throws IOException {
                    int var1 = var0.length;

                    for(int var2 = 0; var2 < var1; ++var2) {
                        MetaIndex.registerDirectory(var0[var2]);
                    }
                    // 創(chuàng)建一個新的ExtClassLoader,傳入文件數(shù)組
                    return new Launcher.ExtClassLoader(var0);
                }
            });
        } catch (PrivilegedActionException var2) {
            throw (IOException)var2.getException();
        }
    }

    void addExtURL(URL var1) {
        super.addURL(var1);
    }

    public ExtClassLoader(File[] var1) throws IOException {
        // 父類構(gòu)造方法,其中第二個參數(shù)為parent也就是當前ClassLoader的父類加載器
        // 這里傳入的是null,也就是其父類加載器是BootStrap ClassLoader
        super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
        SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
    }

    private static File[] getExtDirs() {
        String var0 = System.getProperty("java.ext.dirs");
        File[] var1;
        if(var0 != null) {
            StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
            int var3 = var2.countTokens();
            var1 = new File[var3];

            for(int var4 = 0; var4 < var3; ++var4) {
                var1[var4] = new File(var2.nextToken());
            }
        } else {
            var1 = new File[0];
        }

        return var1;
    }

    ......
}

從代碼中我們可以知曉:

  • ExtClassLoader是在Launcher中創(chuàng)建,并且指定其父類加載器為null(BootStrap ClassLoader)
  • 通過getExtDirs獲得擴展類的目錄文件數(shù)組

我們看下getExtDirs輸出:

C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext
C:\Windows\Sun\Java\lib\ext

這個輸出一個代表了<JAVA_HOME>\lib\ext路徑,另一個則是默認的擴展類路徑。

AppClassLoader的創(chuàng)建

Launcher的部分代碼中可以知道ExtClassLoader作為參數(shù)傳入AppClassLoader中,這里看下AppClassLoader類:

static class AppClassLoader extends URLClassLoader {
    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                // 這里將傳入的ExtClassLoader作為構(gòu)造參數(shù),說明其父類加載器為ExtClassLoader
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }

    AppClassLoader(URL[] var1, ClassLoader var2) {
        super(var1, var2, Launcher.factory);
        this.ucp.initLookupCache(this);
    }

    public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        int var3 = var1.lastIndexOf(46);
        // 加載前的判斷,檢查包權(quán)限以及是否已經(jīng)知道不存在
        if(var3 != -1) {
            SecurityManager var4 = System.getSecurityManager();
            if(var4 != null) {
                var4.checkPackageAccess(var1.substring(0, var3));
            }
        }

        if(this.ucp.knownToNotExist(var1)) {
            Class var5 = this.findLoadedClass(var1);
            if(var5 != null) {
                if(var2) {
                    this.resolveClass(var5);
                }

                return var5;
            } else {
                throw new ClassNotFoundException(var1);
            }
        } else {
            // 調(diào)用ClassLoader的loadClass
            return super.loadClass(var1, var2);
        }
    }

    ......
}

AppClassLoaderExtClassLoader作為父類加載器,并且重寫了loadClass方法,用于校驗。不過我在debug時發(fā)現(xiàn)System.getSecurityManager()返回值為null,所以推測這里需要自己實現(xiàn)安全管理。

驗證:

ClassLoader classLoader = Main.class.getClassLoader();
while (classLoader.getParent() != null) {
    System.out.println(classLoader);
    classLoader = classLoader.getParent();
}
System.out.println(classLoader);

輸出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

類加載器的雙親委派機制

雙親委派機制模型

雙親委派機制:某個特定的類加載器在接到加載類的請求時,首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無法完成此加載任務(wù)時,才自己去加載。
好處:使用雙親委派模型的好處在于Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的Bootstrap ClassLoader進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類并放在ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,程序?qū)⒒靵y。因此,如果開發(fā)者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被加載運行。
(《深入理解Java虛擬機》)

雙親委派機制的實現(xiàn)

廢話不多說,先上代碼為敬:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 第一步檢查此類是否已經(jīng)被加載,native層實現(xiàn)
        Class<?> c = findLoadedClass(name);
        // 如果沒有被加載
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 獲取其父類加載器,并且調(diào)用loadClass()方法
                // 如果父類加載器是BootStrap ClassLoader,則調(diào)用findBootstrapClassOrNull
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 如果沒有加載此類,嘗試通過類名查找此類
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

這里看下loadClass的過程:

  1. 查看類是否已經(jīng)被加載過,通過native方法實現(xiàn)。如果已經(jīng)加載過,直接返回此Class。
  2. 類沒有被加載過,如果其父類加載器存在,調(diào)用父類加載器的loadClass方法加載Class。
  3. 父類加載器不存在,調(diào)用findBootstrapClassOrNull方法查找啟動類加載器是否加載此類,如果有加載則返回;如果沒有加載則調(diào)用findClass方法。
  4. 遞歸的過程中如果有一處得到了Class,那么將返回此Class。

光說不練假把式,還是來舉兩個栗子吧

1. 啟動類加載器加載類

測試代碼:

System.out.println(Provider.class.getClassLoader());

接著將ClassLoader中的findClass設(shè)置斷點,調(diào)試。執(zhí)行結(jié)果如下:

第一次

AppClassLoader

可以看到,第一次執(zhí)行的時候是AppClassLoader進行loadClass方法的調(diào)用。接著進入parent.loadClass方法中:

parent.loadClass

ExtClassLoader

接著調(diào)用了ExtClassLoader中的loadClass方法,我們知道其父類加載器不存在,所以執(zhí)行findBootstrapClassOrNull方法:

findBootstrapClassOrNull

因為我現(xiàn)在挑選的是啟動類加載器加載的類,所以這里面返回值不為空,接著就把此值返回給ExtClassLoader,ExtClassLoader又把值返回給AppClassLoader,最終將值返回,整個過程結(jié)束。

2. 應(yīng)用程序類加載器

測試代碼:

System.out.println(UserModel.class.getClassLoader());

接著將ClassLoader中的findClass設(shè)置斷點,調(diào)試。其查找過程和上面一致,這里不多說,這里需要知道的是此時findBootstrapClassOrNull方法返回值為null,接著會調(diào)用findClass方法:

ExtClassLoader findClass

ExtClassLoader

ExtClassLoader中查找UserModel沒有找到,返回結(jié)果null,緊接著就會調(diào)用AppClassLoaderfindClass方法:
AppClassLoader findClass

AppClassLoader

通過defineClass方法最終獲取到UserModel類,并將結(jié)果返回。

破壞雙親委派機制的自定義類加載器

雙親委派機制是建立在不重寫loadClass流程的基礎(chǔ)上,如果某一個自定義類加載器重寫了loadClass方法,并將其流程改變,那么所謂的雙親委派機制也就消失了。下面的自定義類加載器破壞了雙親委派機制:

public class CustomClassLoader extends ClassLoader {

    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }
    // 重寫了loadClass方法,不用去查找是否加載,如果類文件存在,直接返回所需類
    // 否則按照原方式進行
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        File file = new File(classPath + name.replace(".", "\\") + ".class");
        if (file.exists()) {
            try {
                InputStream is = new FileInputStream(file);
                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return super.loadClass(name);
    }

}
// 測試代碼
private static void test() {
    CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\B-0137\\Desktop\\");
    try {
        Class<?> userModel = customClassLoader.loadClass("com.nick.model.UserModel");
        Object o = userModel.newInstance();
        System.out.println(o);
        System.out.println(o instanceof UserModel);
        IUser iUser = (IUser) o;
        iUser.test();

        Class<?> mC = Main.class.getClassLoader().loadClass("com.nick.model.UserModel");
        Object mainO = mC.newInstance();
        System.out.println(mainO);
        System.out.println(mainO instanceof UserModel);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}

測試結(jié)果:

com.nick.model.UserModel@7f31245a
false
測試
com.nick.model.UserModel@6d6f6e28
true

在這也能看出通過破壞雙親委派機制可以由不同的類加載器加載相同的類,但是他們并不相等——類加載器不同。

保持雙親委派機制的自定義類加載器

其實想要保持雙親委派機制很簡單:只需要在自定義類加載器的時候重寫findClass方法即可。
自定義類加載器這里省略,就是重寫了findClass方法,其他代碼沒變。測試代碼:

private static void test() {
    CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\B-0137\\Desktop\\");
    try {
        Class<?> userModel = customClassLoader.loadClass("com.nick.model.UserModel");
        Object o = userModel.newInstance();
        System.out.println(o);
        System.out.println(o instanceof UserModel);
        System.out.println(userModel.getClassLoader());
        IUser iUser = (IUser) o;
        iUser.test();
        System.out.println();

        Class<?> mC = Main.class.getClassLoader().loadClass("com.nick.model.UserModel");
        Object mainO = mC.newInstance();
        System.out.println(mainO);
        System.out.println(mainO instanceof UserModel);
        System.out.println(mC.getClassLoader());
        System.out.println();
        
        Class<?> userModel2 = customClassLoader.loadClass("com.nick.model.UserModel2");
        Object o2 = userModel2.newInstance();
        System.out.println(o2);
        System.out.println(userModel2.getClassLoader());
        IUser iUser2 = (IUser) o;
        iUser2.test();

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}

測試結(jié)果:

com.nick.model.UserModel@677327b6
true
sun.misc.Launcher$AppClassLoader@18b4aac2
測試

com.nick.model.UserModel@14ae5a5
true
sun.misc.Launcher$AppClassLoader@18b4aac2

com.nick.model.UserModel2@135fbaa4
com.nick.classloader.CustomClassLoader@7f31245a
測試

我們用自定義的類加載器去加載外部的一個和項目中同名的類,結(jié)果發(fā)現(xiàn)其是由應(yīng)用程序類加載器加載,那么可以說明自定義類加載器重寫findclass方法保持了雙親委派機制。

結(jié)尾

作為開年的第一篇文章,洋洋灑灑寫了好多字。從論據(jù)到論點,詳詳細細全部寫完。啰哩啰唆說了一大堆,結(jié)果還沒進入正題(熱修復(fù))。這篇文章主要是為熱修復(fù)打下些基礎(chǔ),下一篇將會講述基于類加載器原理實現(xiàn)的熱修復(fù)以及如何實現(xiàn)。
最后上個美女養(yǎng)養(yǎng)眼吧~

美女
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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