Android源碼學習——ClassLoader(1)

本文學習的源碼參考AndroidXRef,版本爲Lollipop 5.1.0_r1。


類加載與動態加載

Java代碼是通過Class來實現的,程序運行時虛擬機首先將對應的類加載到內存中,然後才能進行對象實例化以及後續工作,而這個加載過程就是通過ClassLoader來實現的,也即是類加載。
同時,在Java虛擬機中,我們可以自定義繼承自ClassLoader的類加載器,通過defineClass方法動態加載一個Class,實現控制程序的類加載過程,提高程序的靈活性。

Android的Dalvik/ART虛擬機與Java虛擬機一樣,採用類加載機制,也具有動態加載技術,但是他們之間還是存在一些差異的。

Android的類加載機制

不管是類加載還是動態加載,它的基礎都是ClassLoader,也即專門用來處理類加載工作的,也稱爲類加載器。而且,一個應用不僅僅只有一個類加載器。

實際上,Android系統啓動時,會創建一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級需要的類,而Android應用裏也需要用到一些系統的類,所以APP啓動的時候也會把這個Boot類型的ClassLoader傳進來。另外,APP也有自己實現的類,這些類保存在APK的dex文件裏面,所以APP啓動的時候,也會創建一個自己的ClassLoader實例,用於加載自己dex文件中的類。

可以通過下面的代碼查看當前程序的類加載器:

    // 獲取當前程序的類加載器
    ClassLoader classLoader = getClassLoader();
    if (classLoader != null){
        Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
        // 獲取當前類加載器的父類加載器
        while (classLoader.getParent()!=null){
            classLoader = classLoader.getParent();
            Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
        }
    }

Android中的ClassLoader是一個抽象類,實際開發過程中,我們一般是使用他的兩個具體的子類DexClassLoader、PathClassLoader這些類加載器來加載類的。他們的主要差別在於:

  • PathClassLoader只能加載系統中已經安裝過的apk;
  • DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk;

看具體代碼:

public class PathClassLoader extends BaseDexClassLoader {

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

    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }

}
public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,
                          String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

}

這兩個類加載器都只是簡單的封裝了BaseDexClassLoader,具體的實現還是調了父類裏的方法。
但是可以看到,PathClassLoader的第二個參數optimizedDirectory只能是null。

繼續看BaseDexClassLoader裏面的實現:

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    ......

}

創建了一個DexPathList實例。

繼續看DexPathList的構造函數的實現:

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

        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }

        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }

        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }

            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }

        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

先進行了一些驗證,然後調用了makeDexElements函數。

    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files up front.
         */
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else if (file.isFile()){
                if (name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException ex) {
                        System.logE("Unable to load dex file: " + file, ex);
                    }
                } else {
                    zip = file;

                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException suppressed) {
                        suppressedExceptions.add(suppressed);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }

這段代碼是爲了生成一個元素爲dex或resource的數組,如果傳入參數是一個目錄則直接new Element(file, true, null, null)添加到數組裏面;
如果傳入參數是一個文件,如.dex、.zip、.jar或.apk,會先執行loadDexFile再new Element(file, false, zip, dex)添加到數組中。
其中,不是直接給出.dex文件的情況下,會將file賦值給zip。

看下loadDexFile做了什麼:

    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }

終於看到optimizedDirectory帶來的差異了。
如果optimizedDirectory爲null,也即PathClassLoader的情況,程序會直接調DexFile的構造函數創建一個DexFile對象;
而DexClassLoader則先去調了一個optimizedPathFor,再去調DexFile的loadDex方法。

看下這個裏面做了什麼:

    private static String optimizedPathFor(File path,
            File optimizedDirectory) {

        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }

        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

解析path中dex的文件名,在optimizedDirectory位置新建了一個文件,返回文件路徑。

    static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {

        return new DexFile(sourcePathName, outputPathName, flags);
    }

調用DexFile的另一個構造函數,返回DexFile對象。

看下這兩個構造函數的區別:

    public DexFile(String fileName) throws IOException {
        mCookie = openDexFile(fileName, null, 0);
        mFileName = fileName;
        guard.open("close");
    }

    private DexFile(String sourceName, String outputName, int flags) throws IOException {
        if (outputName != null) {
            try {
                String parent = new File(outputName).getParent();
                if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                    throw new IllegalArgumentException("Optimized data directory " + parent
                            + " is not owned by the current user. Shared storage cannot protect"
                            + " your application from code injection attacks.");
                }
            } catch (ErrnoException ignored) {
                // assume we'll fail with a more contextual error later
            }
        }

        mCookie = openDexFile(sourceName, outputName, flags);
        mFileName = sourceName;
        guard.open("close");
    }

如果是DexClassLoader,會先檢查輸出路徑outputName父目錄的所有者。但是無論是哪種ClassLoader,最終都會調用openDexFile。

看下openDexFile的實現:

    private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {

        return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                 (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                                 flags);
    }

而openDexFile會直接返回openDexFileNative,而openDexFile是一個native函數,後面再繼續分析。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章