1. 程式人生 > >理解Java ClassLoader & Android ClassLoader

理解Java ClassLoader & Android ClassLoader

1.Java中的ClassLoader

分為兩種型別:系統載入器和自定義類載入器;其中系統載入器包括三種:BootStrap ClassLoader、Extensions ClassLoader和Application ClassLoader。其分別的作用是:

1.1 BootStrap ClassLoader(引導類載入器)

使用C/C++實現的載入器,用於載入指定的JDK核心類庫,像java.lang;java.reflect;java.utils這一類系統類。它用它來載入一下路徑的類庫:

$JAVA_HOME/jre/lib目錄下 -Xbootclasspath引數指定下的目錄

java虛擬機器的啟動時就是通過BootStrap ClassLoader來建立一個初始類來完成的(類似main函式)。由於BootStrap ClassLoader是使用C/C++實現的,所以不能為Java程式碼訪問到。 BootStrap ClassLoader並不沒有繼承java.lang.ClassLoader。

1.2 Extensions ClassLoader(擴充套件類載入器)

java語言中表現為ExtClassLoader,用於載入Java的擴充套件類。在Java9之後,更名為PlatformClassLoader。它提供了除了系統類之外的額外功能,它主要用來載入一下目錄下的類庫:

$JAVA_HOME/jre/lib/ext目錄下 系統屬性java.ext.dir所指定的目錄

1.3 Application ClassLoader(應用程式載入器)

java語言中實現為AppClassLoader,用於載入一下路徑的類庫

當前程式的Classpath目錄 系統屬性java.class.path所指定的目錄

2. 雙親委派模式

java類載入器查詢Class檔案時,就是採用雙親委派模式。基本套路為:

1.先查詢該class檔案時是否載入過,如果載入過,直接返回 2.沒有載入過,先查詢class的任務委派給自己的父載入器 3.如果還沒有找到,就再委派給自己的父類載入器 … 4. 最後將拋給頂層的BootStrap ClassLoader,如果BC找到了就返回;否則就按原路返回,叫子類載入器去載入目標class,如果沒有找到,在叫子子類載入器去載入 5.最終找到就返回,否則丟擲異常

畫圖顯示大致過程: 在這裡插入圖片描述

對於我們需要載入的路勁下的class檔案,此時該class檔案不在當前程式目錄中,此時我們就需要用到了自定義classLoader了。自定義的classLoader需要繼承java.lang.ClassLoader,並複寫loadClass方法。一般尋找class檔案的套路如下:

  1. 自定義classLoader會在自己的快取中尋找該class檔案是否被載入過,如果有直接返回;如果沒有,則將任務委託給我們的AppClassLoader;
  2. 按照上圖的方式進行遞迴查詢;
  3. 一直委託到BootStrap ClassLoader,如果BS的快取中不存在該class檔案,則在$JAVA_HOME/jre/lib或者 -Xbootclasspath引數指定下的目錄 中查詢,如果找到就直接返回Class,沒有則交給子載入器ExtClassLoader查詢;
  4. 同理,ExtClassLoader載入機制到AppClassLoader最後到自定義的CustomClassLoader,最終沒有找到就跑出異常。

2.1 雙親委派的緣由

java的建立者們為什麼會花這麼大力氣來實現這個類的載入機制呢? 主要是為了安全起見!!!如果不使用這種雙親委派模式,我們也可以自定義一個java.lang.String類來代替系統的java.lang.String類,這樣很顯然會造成不可預測的安全隱患,採用雙親委派模式可以使得系統的String類在VM啟動被優先載入,這樣我們自定義的String類將不能代替系統的String類。

ps: java虛擬機器認為兩個類是同一個類需要滿足兩個條件:

  1. 兩個類名一致[packagename + classname]
  2. 被同一個類載入器載入

另外雙親委派還有一個好處就是可以避免類被重複載入,如果該類已經載入過,就沒有必要重複載入一次,直接讀取快取即可。

3. 自定義classLoader

只需要繼承ClassLoader,並重寫findClass即可。自定義的ClassLoader只能過載入指定目錄下的jar包或者Class檔案,所以我們需要指定所在的目錄即可,下面就是一個例子:

import java.io.*;

public class MyClassLoader extends ClassLoader{
    //定義指定的目錄
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("findClass,name:" + name);

        byte[] bytes = loadClassByte(name);
        if(null == bytes) {
            throw new ClassNotFoundException(name + " class not found");
        }
		
		//直接呼叫父類定義的natvive方法載入指定的Class類
        return defineClass(name,bytes,0,bytes.length);
    }

	//將目下下的class檔案轉換為二進位制檔案
    private byte[] loadClassByte(String name){
        String fileName = getFileName(name);

        File file = new File(path, fileName);
        InputStream inputStream = null ;
        ByteArrayOutputStream outputStream = null ;

        byte[] bytes = new byte[1024 * 4];
        int length = -1;
        try {
            inputStream = new FileInputStream(file);
            outputStream = new ByteArrayOutputStream();

            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes,0,length);
            }

            return  outputStream.toByteArray();

        }catch (IOException e){
            e.printStackTrace();
        }finally {
            closeQuite(inputStream);
            closeQuite(outputStream);
        }

        return null ;
    }

    private String getFileName(String name) {
        int index = name.lastIndexOf(".");
        if(index == -1) {
            return name.concat(".class");
        }
        return name.substring(index + 1) .concat(".class");
    }

    private void closeQuite(Closeable closeable){
        if(null != closeable ) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

然後我寫個測試Entity:

package com.xing;

public class Person {

    private String name ;
    public int age ;

    public Person(String name , int age) {
        this.name = name;
        this.age = age ;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

我們再來寫個測試:

    public static void main(String[] args) throws Exception {
        //目錄自己定義
        MyClassLoader classLoader = new MyClassLoader("D:\\java\\idea\\work\\out\\production\\work\\com\\xing");
        Class clazz = classLoader.findClass("com.xing.Person");

        Object object = clazz.getDeclaredConstructor(String.class, int.class).newInstance("zhangsan", 18);

        System.out.println(object);
        
        System.out.println(Person.class.getClassLoader());
        System.out.println(object.getClass().getClassLoader());
        System.out.println(object instanceof Person);
    }

我們看一下輸出:

findClass,name:com.xing.Person
Person{name='zhangsan', age=18}
jdk.internal.loader.ClassLoaders$AppClassLoader@726f3b58
com.xing.MyClassLoader@58ceff1
false

看最後兩行,我們可以看出使用自定義的ClassLoader可以載入指定的Class檔案,最後一行可以看出object instanceOf Person顯示結果是false,即二者不是同一個類。因為兩個類的classloader是不一致的,所以說明判斷兩個類是否一致不僅需要看類名,還需要看兩者的類載入器是否一致。

4. Andorid中的ClassLoader

4.1 在Android中ClassLoader的型別

和Java中不一致,Java虛擬機器中載入的是class檔案,而Android中載入的是dex檔案。二者是有區別的。Android中classLoader也分為兩種型別:系統載入器和自定義載入器。其中系統載入器分為BootClassLoader、PathClassLoader和DexClassLoader。

4.2 BootClassLoader

Android系統啟動時會使用BootClassLoader來載入常用類,與Java的BootStrap ClassLoader不同,BootClassLoader使用的是java編寫的:

### /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
1336 class BootClassLoader extends ClassLoader {
1337
1338    private static BootClassLoader instance;
1339
1340    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
1341    public static synchronized BootClassLoader getInstance() {
1342        if (instance == null) {
1343            instance = new BootClassLoader();
1344        }
1345
1346        return instance;
1347    }

BootClassLoader是ClassLoader的內部類,並繼承了ClassLoader.BootClassLoader是一個單例類。而且BootClassLoader的修飾符是預設的,只能在同一個包可以訪問,因此在外部是不能呼叫的。

4.3 DexClassLoader

DexClassLoader可以載入dex檔案以及包含dex的壓縮檔案(apk和jar檔案),DexClassLoader的程式碼如下:

####  /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
36public class DexClassLoader extends BaseDexClassLoader {
37    /**
38     * Creates a {@code DexClassLoader} that finds interpreted and native
39     * code.  Interpreted classes are found in a set of DEX files contained
40     * in Jar or APK files.
41     *
42     * <p>The path lists are separated using the character specified by the
43     * {@code path.separator} system property, which defaults to {@code :}.
44     *
45     * @param dexPath the list of jar/apk files containing classes and
46     *     resources, delimited by {@code File.pathSeparator}, which
47     *     defaults to {@code ":"} on Android
48     * @param optimizedDirectory directory where optimized dex files
49     *     should be written; must not be {@code null}
50     * @param librarySearchPath the list of directories containing native
51     *     libraries, delimited by {@code File.pathSeparator}; may be
52     *     {@code null}
53     * @param parent the parent class loader
54     */
55    public DexClassLoader(String dexPath, String optimizedDirectory,
56            String librarySearchPath, ClassLoader parent) {
57        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
58    }
59}

引數為:

  1. dexPath jar/apk檔案路徑的集合,預設分割符為: 2.optimizedDirectory 解壓dex檔案的路徑 3.librarySearchPath 本地檔案庫(c/c++檔案) parent 父classLoader

4.4 PathClassLoader

載入系統類和應用程式類

#### /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
25 public class PathClassLoader extends BaseDexClassLoader {
26    /**
27     * Creates a {@code PathClassLoader} that operates on a given list of files
28     * and directories. This method is equivalent to calling
29     * {@link #PathClassLoader(String, String, ClassLoader)} with a
30     * {@code null} value for the second argument (see description there).
31     *
32     * @param dexPath the list of jar/apk files containing classes and
33     * resources, delimited by {@code File.pathSeparator}, which
34     * defaults to {@code ":"} on Android
35     * @param parent the parent class loader
36     */
37    public PathClassLoader(String dexPath, ClassLoader parent) {
38        super(dexPath, null, null, parent);
39    }
40
41    /**
42     * Creates a {@code PathClassLoader} that operates on two given
43     * lists of files and directories. The entries of the first list
44     * should be one of the following:
45     *
46     * <ul>
47     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
48     * well as arbitrary resources.
49     * <li>Raw ".dex" files (not inside a zip file).
50     * </ul>
51     *
52     * The entries of the second list should be directories containing
53     * native library files.
54     *
55     * @param dexPath the list of jar/apk files containing classes and
56     * resources, delimited by {@code File.pathSeparator}, which
57     * defaults to {@code ":"} on Android
58     * @param librarySearchPath the list of directories containing native
59     * libraries, delimited by {@code File.pathSeparator}; may be
60     * {@code null}
61     * @param parent the parent class loader
62     */
63    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
64        super(dexPath, null, librarySearchPath, parent);
65    }
66}

5. ClassLoader的繼承關係

在Andorid程式碼中,列印類載入器:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ClassLoader loader = MainActivity.this.getClassLoader();
        while (null != loader) {
            Log.d("TAG" , "--->>" + loader);
            loader = loader.getParent();
        }
    }

列印的結果為:

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/base.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_dependencies_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_resources_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_0_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_1_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_2_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_3_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_4_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_5_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_6_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_7_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_8_apk.apk", 
zip file "/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/split_lib_slice_9_apk.apk"],
nativeLibraryDirectories=[/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/lib/x86, /system/lib, /vendor/lib]]]

 D/TAG: --->>java.lang.BootClassLoader@cc90c73

可以看出存在兩種類載入器,一種是PathClassLoader,另外一種是BootClassLoader. dexPathList中包含了很多apk的路徑,其中/data/app/com.yincheng.androidtest-12TPNGHK_JPvV-NhgR5x6g==/base.apk

6. Android中雙親委派過程

同樣需要檢視ClassLoader的findClass方法:

/** <@link> 
http://androidxref.com/8.0.0_r4/xref/libcore/ojluni/src/main/java/java/lang/ClassLoader.java#180
*/
359    protected Class<?> loadClass(String name, boolean resolve)
360        throws ClassNotFoundException
361    {
362            // First, check if the class has already been loaded
363            Class<?> c = findLoadedClass(name);
364            if (c == null) {
365                try {
366                    if (parent != null) {
367                        c = parent.loadClass(name, false);
368                    } else {
369                        c = findBootstrapClassOrNull(name);  //return null at the time
370                    }
371                } catch (ClassNotFoundException e) {
372                    // ClassNotFoundException thrown if class not found
373                    // from the non-null parent class loader
374                }
375
376                if (c == null) {
377                    // If still not found, then invoke findClass in order
378                    // to find the class.
379                    c = findClass(name);
380                }
381            }
382            return c;
383    }

大致過程上面註釋已經很清楚了,如果沒有找到,就委託給父類載入器;如果都沒有,則執行findClass,其中findClass中:

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

看來需要子類來實現了,我們看一下它的子類BaseDexClassLoader的findClass方法:

### http://androidxref.com/8.0.0_r4/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java#30

88    @Override
89    protected Class<?> findClass(String name) throws ClassNotFoundException {
90        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
91        Class c = pathList.findClass(name, suppressedExceptions);
92        if (c == null) {
93            ClassNotFoundException cnfe = new ClassNotFoundException(
94                    "Didn't find class \"" + name + "\" on path: " + pathList);
95            for (Throwable t : suppressedExceptions) {
96                cnfe.addSuppressed(t);
97            }
98            throw cnfe;
99        }
100        return c;
101    }

可以看出是由pathList中獲取的,那麼這個pathList是從哪裡來的呢?是從構造方法裡面傳入的:

 ### http://androidxref.com/8.0.0_r4/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java#30
 
82    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
83        // TODO We should support giving this a library search path maybe.
84        super(parent);
85        this.pathList = new DexPathList(this, dexFiles);
86    }

然後我們看一下DexPathList中findClass方法:

### http://androidxref.com/8.0.0_r4/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java#86

464    public Class<?> findClass(String name, List<Throwable> suppressed) {
465        for (Element element : dexElements) {
466            Class<?> clazz = element.findClass(name, definingContext, suppressed);
467            if (clazz != null) {
468                return clazz;
469            }
470        }
471
472        if (dexElementsSuppressedExceptions != null) {
473            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
474        }
475        return null;
476    }
477

然後再看Element中findClass的方法:

    /*package*/ static class Element {
675        public Class<?> findClass(String name, ClassLoader definingContext,
676                List<Throwable> suppressed) {
677            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
678                    : null;
679        }

這次是dexFile來loadClassBinaryName了,我們查詢一下這個DexFile:

### http://androidxref.com/8.0.0_r4/xref/libcore/dalvik/src/main/java/dalvik/system/DexFile.java#42

274    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
275        return defineClass(name, loader, mCookie, this, suppressed);
276    }
277
278    private static Class defineClass(String name, ClassLoader loader, Object cookie,
279                                     DexFile dexFile, List<Throwable> suppressed) {
280        Class result = null;
281        try {
282            result = defineClassNative(name, loader, cookie, dexFile);
283        } catch (NoClassDefFoundError e) {
284            if (suppressed != null) {
285                suppressed.add(e);
286            }
287        } catch (ClassNotFoundException e) {
288            if (suppressed != null) {
289                suppressed.add(e);
290            }
291        }
292        return result;
293