關(guān)于ClassLoader,你需要了解的

Java中的ClassLoader

Java中包含三種系統(tǒng)類加載器,分別是Bootstrap ClassLoader、Extensions ClassLoader和Application ClassLoader。除此之外,Java還支持自定義的ClassLoader。

Bootstrap ClassLoader

Bootstrap ClassLoader也稱為引導(dǎo)類加載器,它是由C/C++代碼實現(xiàn)的類加載器,用于加載JDK的核心類庫,比如java.lang、java.uti等這些系統(tǒng)類。它用來加載以下目錄下的類庫:

  • $JAVA_HOME/jre/lib目錄
  • -Xbootclasspath參數(shù)指定的目錄

Java虛擬機的啟動就是通過Bootstrap ClassLoader創(chuàng)建第一個初始類來完成的。由于Bootstrap ClassLoader是用C/C++語言實現(xiàn)的,所以該加載器不能被Java代碼訪問到。需要注意的是,Bootstrap ClassLoader并不是繼承自java.lang.ClassLoader。

Extensions ClassLoader

Extensions ClassLoader也稱為擴展類加載器。在Java中的實現(xiàn)類是ExtClassLoader。它用于加載Java的擴展類,提供除了系統(tǒng)類之外的一些功能。ExtClassLoader用來加載以下目錄下的類庫:

  • $JAVA_HOME/jre/lib/ext目錄
  • 系統(tǒng)屬性java.ext.dir所指定的目錄
Application ClassLoader

也稱為應(yīng)用程序類加載器,在java中的實現(xiàn)類是AppClassLoader,因此簡稱AppClassLoader。同時它又可以稱為系統(tǒng)類加載器(System ClassLoader),這是因為AppClassLoader可以通過ClassLoader.getSystemClassLoader方法獲取到。它用來加載以下目錄下的類庫:

  • 當(dāng)前程序的Classpath目錄
  • 系統(tǒng)屬性java.class.path指定的目錄
自定義ClassLoader

自定義ClassLoader通過繼承java.lang.ClassLoader類的方式來實現(xiàn)自己的類加載器。ExtCLassLoader和AppClassLoader也繼承自java.lang.ClassLoader。實現(xiàn)自定義ClassLoader需要兩個步驟

  1. 定義一個自定義ClassLoader并繼承抽象類ClassLoader。
  2. 重新findClass方法,并在方法中調(diào)用defineClass方法。

ClassLoader的繼承關(guān)系

運行一個Java程序需要用到幾種類型的ClassLoader呢

IGame the_last_of_us = new IGame();
        ClassLoader loader = the_last_of_us.getClass().getClassLoader();
        while(loader != null){
            System.out.println(loader);
            loader = loader.getParent();
        }

通過運行以上代碼可以看到,輸出結(jié)果是

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@135fbaa4

第一行說明加載IGame類的是AppClassLoader,第二行輸出說明AppClassLoader通過getParent()方法獲取到的是ExtClassLoader,這并不表示它們是父類和子類的繼承關(guān)系,只能說ExtClassLoader是AppClassLoader的父加載器。至于為何沒有打印出ExtClassLoader的父加載器Bootstrap ClassLoader,只是因為Bootstrap ClassLoader是由C/C++語言實現(xiàn)的,并不是一個Java類,無法在Java代碼中獲取它的引用。系統(tǒng)提供的類加載器有這三種類型,但并不是說系統(tǒng)系統(tǒng)的ClassLoader只有這3個。ClassLoader的繼承關(guān)系如下圖所示:
Java中ClassLoader的繼承關(guān)系

可以看出一共有5個相關(guān)類:

  • ClassLoader是一個抽象類,其中定義了ClassLoader的主要功能
  • SecureClassLoader繼承了抽象類ClassLoader,但SecureClassLoader并不是ClassLoader的實現(xiàn)類,而是擴展了ClassLoader類,加入了權(quán)限方面的功能,加強了ClassLoader的安全性。
    URLClassLoader繼承自SecureClassLoader,可以通過URL路徑從jar文件和文件夾中加載類和資源
  • ExtClassLoader和AppClassLoader都是繼承自URLClassLoader,他們都是Launcher的內(nèi)部類,Launcher是Java虛擬機的入口應(yīng)用,ExtClassLoader和AppClassLoader都是在Launcher中進行初始化的。

雙親委派模式

類加載器查找Class所采用的是雙親委派模式,所謂雙親委派模式,就是首先判斷該Class是否已經(jīng)加載,如果沒有并不是該加載器直接去查找,而是委托給父加載器進行查找,這樣依次遞歸直到委托到最頂層的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了該Class,就會直接加載并返回。如果沒找到,則繼續(xù)依次向下查找,如果還沒找到則最后會交給最初判斷的類加載器去查找。具體過程如下圖所示:
雙親委托模式

以一個具體的例子來說,假設(shè)我們要加載一個位于D盤的Class文件,這時系統(tǒng)提供的類加載器不能滿足條件,這時就需要我們自定義類加載器繼承自java.lang.ClassLoader。并重寫它的findClass方法。按照雙親委派流程,加載過程如下:

  1. 自定義的類加載器首先在緩存中查找Class文件是否已經(jīng)加載,如果已經(jīng)加載就返回該Class。否則就委托給父加載器,也就是AppClassLoader。
  2. 按照上圖中虛線的方向遞歸步驟一,即AppClassLoader在緩存中查找該Class文件是否已經(jīng)加載,如果已經(jīng)加載就返回該Class。否則就委托給它的父加載器ExtClassLoader。
  3. 一直委托到Bootstrap ClassLoader,如果BootStrap ClassLoader查找緩存也沒有加載該Class文件,則在$JAVA_HOME/jre/lib目錄或者-Xbootclasspath參數(shù)指定的目錄中進行查找,如果找到就加載該Class并返回。如果沒有找到則交給它的子加載器ExtClassLoader。
  4. ExtClassLoader會在$JAVA_HOME/jre/lib/ext目錄或系統(tǒng)屬性java.ext.dir所指定的目錄中進行查找,如果找到該Class就加載并返回。否則就交給AppClassLoader。
  5. AppClassLoader會在當(dāng)前程序的Classpath目錄或系統(tǒng)屬性java.class.path指定的目錄中進行查找,如果找到該Class就加載并返回,找不到則交給我們自定義的類加載器,如果還找不到就會拋出異常。

總的來說就是Class文件加載到ClassLoader子系統(tǒng)后,先沿著上圖中虛線方向自下而上進行委托,判斷該Class是否已加載,如果沒有加載,再沿著實線方向自上而下進行查找和加載。結(jié)合上一節(jié)講的ClassLoader的繼承關(guān)系,可以得出ClassLoader子系統(tǒng)中的父子關(guān)系并不是使用繼承實現(xiàn),而是使用組合來實現(xiàn)代碼復(fù)用的。
類加載的過程在JDK8的源碼中也能看出雙親委派模式的邏輯實現(xiàn)。以下是抽象類ClassLoader的loadClass方法的源碼。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    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;
        }
    }

首先調(diào)用findLoadedClass從已加載的類中檢查目標(biāo)類是否已加載,如果未加載即c為空,判斷parent(父加載器)是否存在,如果存在就調(diào)用父加載器的loadClass方法,否則就調(diào)用findBootstrapClassOrNull方法,這個方法內(nèi)部會調(diào)用Native方法findBootstrapClass,findBootstrapClass方法中最終會用Bootstrap ClassLoader來檢查目標(biāo)類是否已經(jīng)加載,如果沒有加載就說明向上委托流程中沒有發(fā)現(xiàn)目標(biāo)類已加載。然后調(diào)用findClass方法繼續(xù)向下進行查找流程。
采取雙親委派模式主要有兩個好處:

  • 避免重復(fù)加載,如果該Class已經(jīng)加載過一次,就不需要再次加載,而是從緩存中讀取已經(jīng)加載的Class信息。
  • 更加安全,如果不使用雙親委派模式,就可以自定義一個String類來替代系統(tǒng)的String類,這顯然是會造成安全隱患的,采用雙親委派模式會是的系統(tǒng)的String類在Java虛擬機啟動時就被加載,也就無法用自定義的String類來替代系統(tǒng)的String類。除非我們修改類加載器的默認(rèn)搜索算法。還有一點,只有兩個類名一致且被同一個類加載器加載的類,JVM才會認(rèn)為它們是同一個類,想要騙過JVM顯然沒那么容易。

Android中的ClassLoader

可能有些同學(xué)認(rèn)為,Android中的ClassLoader和Java中的ClassLoader是一樣的,這顯然是不對的。下文中會介紹兩者之間有何不同。

ClassLoader的類型

Java中的ClassLoader可以加載class文件和jar文件,本質(zhì)上都是加載class文件。這一點在Android中并不適用,因為無論是DVM還是ART,他們加載的不再是class文件,而是dex文件,這就需要重新設(shè)計ClassLoader的相關(guān)類。Android中的ClassLoader有3種系統(tǒng)默認(rèn)的類型,分別是BootClassLoader、PathClassLoader和DexClassLoader,除此之外,Android也支持自定義的ClassLoader。

BootClassLoader

Android系統(tǒng)啟動時會使用BootClassLoader來預(yù)加載常用類,與JDK中的Bootstrap ClassLoader不同,它并不是由C/C++代碼實現(xiàn)的,而是用Java代碼實現(xiàn)的。BootClassLoader的代碼如下

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }
...

BootClassLoader是ClassLoader的內(nèi)部類,這里的ClassLoader并不是JDK里的ClassLoader,這一點需要注意。BootClassLoader是一個單例類,需要注意的是BootClassLoader的訪問修飾符是默認(rèn)的,即只有同一個包中的類才能訪問,因此我們在應(yīng)用程序中是無法直接調(diào)用的。關(guān)于BootClassLoader的創(chuàng)建,BootClassLoader是在Zygote進程的ZygoteInit的入口方法中創(chuàng)建的,用于加載preloaded-classes文件中存有的預(yù)加載類。

DexClassLoader

DexClassLoader可以加載dex文件以及包含dex的壓縮文件,如apk文件和jar文件,不管是哪種文件,最終要加載的都是dex文件。查看DexClassLoader的源碼如下

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     *     resources, delimited by {@code File.pathSeparator}, which
     *     defaults to {@code ":"} on Android
     * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
     * @param librarySearchPath the list of directories containing native
     *     libraries, delimited by {@code File.pathSeparator}; may be
     *     {@code null}
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

DexClassLoader類的定義中只包含一個構(gòu)造方法,有4個參數(shù):

  • dexPath:dex相關(guān)文件的路徑集合,多個路徑用文件分隔符隔開,默認(rèn)的分隔符是“:”。
  • optimizedDirectory:解壓的dex文件存儲路徑,這個路徑必須是一個內(nèi)部存儲路徑。一般情況下,使用當(dāng)前應(yīng)用程序的私有路徑:/data/data/<Package Name>/...
  • librarySearchPath:包含C/C++庫的路徑集合,多個路徑用文件分隔符隔開,可以為null。
  • parent:父加載器

DexClassLoader繼承自BaseDexClassLoader,方法都在BaseDexClassLoader中實現(xiàn)。

PathClassLoader

Android系統(tǒng)使用PathClassLoader來加載系統(tǒng)類和應(yīng)用程序的類,查看其代碼如下

public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

PathClassLoader也繼承自BaseDexClassLoader,方法也都在BaseDexClassLoader中實現(xiàn)。在PathDexClassLoader的構(gòu)造方法中沒有optimizedDirectory參數(shù),這是因為PathClassLoader已經(jīng)默認(rèn)設(shè)置了optimizedDirectory參數(shù)的值為/data/dalvik-cache,這也意味著PathClassLoader無法定義解壓的dex文件存儲路徑,因此PathClassLoader通常用來加載已經(jīng)apk的dex文件,已安裝的apk的dex文件會存儲在/data/dalvik-cache目錄中。PathClassLoader的創(chuàng)建,是在Zygote進程創(chuàng)建SystemServer進程后,在SystemServer進程中采用工廠模式創(chuàng)建的。

ClassLoader的繼承關(guān)系

Android中ClassLoader的繼承關(guān)系

從圖中可以看出一共有8個ClassLoader相關(guān)類,其中有一些和Java中的ClassLoader相關(guān)類十分相似。

  • ClassLoader 是一個抽象類,其中定義了ClassLoader的主要功能。BootClassLoader是它的內(nèi)部類。
  • SecureClassLoader類和JDK8中的SecureClassLoader類的代碼一樣,它繼承了抽象類ClassLoader。SecureClassLoader并不是ClassLoader的實現(xiàn)類,而是擴展了ClassLoader類,加入了權(quán)限方面的功能,加強了ClassLoader的安全性。
  • URLClassLoader類和JDK8中的URLClassLoader類的代碼一樣,他繼承自SecureClassLoader,用來通過URL路徑從jar文件和文件夾中加載指定的資源。
  • InMemoryDexClassLoader 是Android8.0新增的CLassLoader,繼承自BaseDexClassLoader,用來加載內(nèi)存中的dex文件
  • BaseDexClassLoader繼承自ClassLoader,是抽象類ClassLoader的具體實現(xiàn)類,PathCLassLoader、DexClassLoader和InMemoryDexClassLoader都是繼承自BaseDexClassLoader。

ClassLoader的加載過程

Android中的ClassLoader同樣遵循雙親委派模式,ClassLoader的加載方法為loadClass方法,這個方法定義在抽象類ClassLoader中,代碼如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    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.
                    c = findClass(name);
                }
            }
            return c;
    }

可以看出,loadClass方法的代碼邏輯和JDK中ClassLoader類的loadClass方法非常相似,此處不再贅述,重點在findClass方法。

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

抽象類ClassLoader的findClass方法中直接拋出了異常,說明findClass方法需要子類來實現(xiàn),BaseDexClassLoader的代碼如下:

public class BaseDexClassLoader extends ClassLoader {
  ...
  private final DexPathList pathList;
  ...
  public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
  ...
}

在BaseDexClassLoader的構(gòu)造方法中,創(chuàng)建了DexPathList。在findClass方法中調(diào)用了DexPathList的findClass方法。代碼如下

public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

DexPathList的findClass方法中會遍歷Element類型的數(shù)組dexElements,然后調(diào)用Element的findClass方法。Element是DexPathList的內(nèi)部類:

static class Element {
        /**
         * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
         * (only when dexFile is null).
         */
        private final File path;

        private final DexFile dexFile;

        private ClassPathURLStreamHandler urlHandler;
        private boolean initialized;

        /**
         * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
         * should be null), or a jar (in which case dexZipPath should denote the zip file).
         */
        public Element(DexFile dexFile, File dexZipPath) {
            this.dexFile = dexFile;
            this.path = dexZipPath;
        }

        public Element(DexFile dexFile) {
            this.dexFile = dexFile;
            this.path = null;
        }

        public Element(File path) {
          this.path = path;
          this.dexFile = null;
        }
        ...
        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }
}

從Element的構(gòu)造方法來看,其內(nèi)部封裝了DexFile,每一個Element對象都包含一個DexFile對象,它用來加載dex文件。Element的findClass方法內(nèi)部判斷,如果DexFile不為null就調(diào)用DexFile的loadClassBinaryName方法。

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }
private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

DexFile的loadClassBinaryName方法直接調(diào)用了defineClass方法,而在defineClass方法內(nèi)部調(diào)用了defineClassNative方法來加載dex文件,這個方法是個Native方法,有興趣的同學(xué)可以自行查看其源碼。Android中ClassLoader的加載就是遵循著雙親委派模式,如果委托過程中沒有檢查到此前加載過目標(biāo)類,就調(diào)用ClassLoader的findClass方法,在Java層最終會調(diào)用DexFIle的defineClassNative方法來執(zhí)行查找流程。

本文參考
《Android進階解密》

最后編輯于
?著作權(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ù)。

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