1. 程式人生 > >關於DexClassLoader和PathClassLoader,以及Dalvik載入類的過程

關於DexClassLoader和PathClassLoader,以及Dalvik載入類的過程

android中,dalvik虛擬機器載入的是dex檔案,用於載入類的ClassLoader是PathClassLoader和DexClassLoader。PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,它們的父ClassLoader為BootClassLoader。

回顧在JVM中,自定義的ClassLoader一般直接繼承自ClassLoader類,為了滿足雙親委派模型,重寫findClass類來將類的全限定名轉化為Class。在findClass中,一般會呼叫ClassLoader的defineClass方法來將byte[]形式的JVM位元組碼轉化為對應的Class。

但是在dalvik虛擬機器上,JVM的位元組碼是無法執行在上面執行的,所以不能通過defineClass來生成dalvik所需要的類。dalvik為了解決這個問題,在android上載入類的ClassLoader主要為PathClassLoader和DexClassLoader,這兩種ClassLoader通過讀取dex再呼叫一些native的方法,可以載入程式執行時所需要的類。

在Android Sdk中的PathClassLoader和DexClassLoader無法看到原始碼,所以想要看具體原始碼實現,可以去看android_libcore的原始碼

DexClassLoader和PathClassLoader的異同

	public class PathClassLoader extends BaseDexClassLoader {
		public PathClassLoader(String dexPath, ClassLoader parent) {
	        super(dexPath, null, null, parent);
	    }

	    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
	        super(dexPath, null, librarySearchPath,
parent); } } public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); } } }

從上面看出,兩種ClassLoader在實現上只是構造方法不同:PathClassLoader在呼叫父類構造方法時optimizedDirectory傳入為null;DexClassLoader卻傳了一個new File(optimizedDirectory)進去。

optimizedDirectory是什麼

    /**
     * Constructs an instance.
     *
     * @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 directory where optimized dex files
     * should be written; may be {@code null}
     * @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 BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

註釋中說optimizedDirectory是optimized dex的路徑,也就是傳說中的odex。也就是說,我們可以將自己指定的odex路徑作為引數傳入DexClassLoader,odex中的相關類將被載入。但是PathClassLoader在構造方法中,已經將optimizedDirectory寫死為null了,所以PathClassLoader從原則上是無法載入使用者指定的odex的。

BUT!!!,在BaseDexClassLoader中有一個方法:

    /**
     * @hide
     */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }

這個方法從字面意思上可以手動新增一個dexPath路徑,從而在類載入時會查詢這個路徑。只是這個方法是個hide方法,個人認為addDexPath不推薦使用者呼叫是為了安全性考慮的。試想,你吃著火鍋,唱著歌,突然就被麻匪截了。。不對,是dexPath路徑已經解析完成了,程式跑的好好的突然你的dexPath不知道被哪個王八蛋玩意給你加了一個,而且還是個原來已經定義的方法,程式突然就crash了。。。

進入addDexPath檢視一下邏輯:

    public void addDexPath(String dexPath, File optimizedDirectory) {
        final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
        final Element[] newElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                suppressedExceptionList, definingContext);

        if (newElements != null && newElements.length > 0) {
            final Element[] oldElements = dexElements;
            dexElements = new Element[oldElements.length + newElements.length];
            System.arraycopy(
                    oldElements, 0, dexElements, 0, oldElements.length);
            System.arraycopy(
                    newElements, 0, dexElements, oldElements.length, newElements.length);
        }

        if (suppressedExceptionList.size() > 0) {
            final IOException[] newSuppressedExceptions = suppressedExceptionList.toArray(
                    new IOException[suppressedExceptionList.size()]);
            if (dexElementsSuppressedExceptions != null) {
                final IOException[] oldSuppressedExceptions = dexElementsSuppressedExceptions;
                final int suppressedExceptionsLength = oldSuppressedExceptions.length +
                        newSuppressedExceptions.length;
                dexElementsSuppressedExceptions = new IOException[suppressedExceptionsLength];
                System.arraycopy(oldSuppressedExceptions, 0, dexElementsSuppressedExceptions,
                        0, oldSuppressedExceptions.length);
                System.arraycopy(newSuppressedExceptions, 0, dexElementsSuppressedExceptions,
                        oldSuppressedExceptions.length, newSuppressedExceptions.length);
            } else {
                dexElementsSuppressedExceptions = newSuppressedExceptions;
            }
        }
    }

主要乾的事情有3件:

  1. 根據dexPath和自定義的optimizedDirectory,通過makeDexElements生成dexFile相關的結點。
  2. 將新的Element結點們新增到原來的dexElements中。
  3. 如果在makeDexElements有錯誤生成,就會把這些錯誤異常儲存起來,放在dexElementsSuppressedExceptions中。

值得注意的是,關於makeDexElements在整個DexPathList中存在兩處被呼叫的地方:一處是剛剛說的addDexPath方法,另一處是構造方法。這也說明了,為什麼BaseDexClassLoader中的addDexPath為什麼是hide的。原因是希望dex查詢路徑在ClassLoader建立時已經確定,後期如果新增會有不確定性的風險。

關於BaseDexClassLoader

不論是DexClassLoader還是PathClassLoader,都是繼承自BaseDexClassLoader。在BaseDexClassLoader中,按照自定義ClassLoader的江湖慣例,實現的是findClass方法,並沒有去實現loadClass方法,所以不論是DexClassLoader還是PathClassLoader都是滿足雙親委派模型的。

看下findClass的實現:

    @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;
    }

邏輯很簡單,真正實現類的全限定名到載入後的Class轉化的是DexPathList的findClass方法,pathList是在BaseDexClassLoader的構造方法中初始化的。

DexPathList的findClass方法:

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

DexPathList的findClass方法會對dexElements中的每一個element,並通過loadClassBinaryName嘗試進行類載入,一旦載入成功,就會立即返回成功的結果。dexElements是通過makeDexElements產生的。而makeDexElements在上文中提到,只在DexPathList的構造方法和addDexPath方法被呼叫到。而addDexPath作為hide方法有可能不被呼叫,所以可以理解dexElements是在構造方法通過makeDexElements生成的。

findClass中有一個引數是suppressed,這個引數是用於收集從ClassLoader初始化到findClass這個過程中出現的全部的異常。由於類載入時,可能會存在各種各樣的異常資訊,但是當異常發生時DexPathList並沒有立即丟擲異常,而是先catch住,並將這個異常存放在dexElementsSuppressedExceptions這個成員變數中。知道findClass時,通過傳入的suppressed引用返回這個過程中的所有異常。

所以到了現在,理解davik載入類的過程主要就差知道三件事情了:

  1. DexPathList在建構函式初始化時做了什麼
  2. makeDexElements做了什麼
  3. DexFile是怎樣通過loadClassBinaryName生成Class的

DexPathList在建構函式初始化時做了什麼

public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {

        //...省略...
        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);

        // Native libraries may exist in both the system and
        // application library paths, and we use this search order:
        //
        //   1. This class loader's library path for application libraries (librarySearchPath):
        //   1.1. Native library directories
        //   1.2. Path to libraries in apk-files
        //   2. The VM's library path from the system property for system libraries
        //      also known as java.library.path
        //
        // This order was reversed prior to Gingerbread; see http://b/2933456.
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
                                                          suppressedExceptions,
                                                          definingContext);

        //...省略...
    }

一方面是makeDexElements生成了dexElements節點資訊,供findClass時載入;另一方法是將傳入引數的librarySearchPath和系統中java.library.path的路徑合併,共同通過makePathElements生成native類路徑的結點資訊,存放在nativeLibraryPathElements中。nativeLibraryPathElements主要是用於ClassLoader的findLibrary方法獲取系統類的載入路徑。

makeDexElements做了什麼

程式碼略長,makeDexElements主要是根據傳入的類路徑檔名和odex檔案,生成Element結點。類路徑檔名有可能在apk這樣的壓縮檔案中,這樣的路徑中會帶有"!/"這樣的分隔符。也有可能是很原始的dex檔案,甚至是一個資料夾。總之makeElements會根據類路徑檔名生成Element結點儲存在elements中。Element中有一個成員變數是dexFile,DexFile會最終生成Class。

DexFile是怎樣通過loadClassBinaryName生成Class

DexFile會通過loadClassBinaryName,最終呼叫到defineClassNative的方法,並返回Class。defineClassNative是一個Native方法,再之後就是dalvik從平臺層面的原生實現了。

    private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                                  DexFile dexFile)

所以有一個問題出現了,因為在JVM虛擬機器中可以呼叫defineClass來載入所需要的類,如果在dalvik上呼叫defineClass會發生什麼?

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        throw new UnsupportedOperationException("can't load this type of class file");
    }

直接拋了UnsupportedOperationException異常。所以雖然和JVM同樣都是雙親委派,但是在具體實現方式上有所不同,因為dalvik的位元組碼和JVM完全不同!dalvik是基於暫存器的虛擬機器,在指令集上更像是8086的彙編;而JVM是基於暫存器的,執行指令時會將運算元和操作符放在棧幀中,返回結果也會返回到棧幀的頂部。

只是天無絕人之路,dalvik同樣有自定義動態載入類的需求,所以才有了DexClassLoader,最終通過defineClassNative這個方法我們想要的類。

總結

我們可以看出,DexClassLoader是通過optimizedDirectory這個引數指定odex的位置,並在初始化時初始化DexPathList,並根據載入路徑與自定義的odex的位置生成Element結點陣列儲存在DexPathList中。通過雙親委派模型的規則,當發現某個類沒有被載入且沒有被父ClassLoader載入時,會呼叫到BasePathClassLoader的findClass方法。然後逐一嘗試載入Element結點陣列中每一個結點的DexFile檔案,呼叫loadClassBinaryName並最終呼叫到defineClassNative這個native方法返回所需要的Class。