Android插件化之ClassLoader

ClassLoader是由JVM平臺提供的類加載器。它容許程序從網絡、硬盤甚至是內存加載Class,這就爲Android插件化提供了最基礎的技術保障。Android平臺對字節碼文件做了優化,摒棄了傳統JVM須要的.jar文件,而是採用體積更小的.dex文件。所以,Android自定義了一系列ClassLoader以知足對dex加載。本文分爲兩部分,第一部分介紹Android的ClassLoader機制;第二部分介紹Android ClassLoader機制在插件化中的應用。java

Android的ClassLoader機制

類加載機制

爲了表述方便,咱們先來看一下《深刻理解Java虛擬機》是對類加載機制怎麼描述的:android

Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校檢、轉換解析和初始化的,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。 與那些在編譯時進行鏈鏈接工做的語言不一樣,在Java語言裏面,類型的加載、鏈接和初始化都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增長一些性能開銷,可是會爲Java應用程序提供高度的靈活性,Java裏天生能夠同代拓展的語言特性就是依賴運行期動態加載和動態連接這個特色實現的。例如,若是編寫一個面相接口的應用程序,能夠等到運行時在制定實際的實現類;用戶能夠經過Java與定義的和自定義的類加載器,讓一個本地的應用程序能夠在運行時從網絡或其餘地方加載一個二進制流做爲代碼的一部分,這種組裝應用程序的方式目前已經普遍應用於Java程序之中。從最基礎的Applet,JSP到複雜的OSGi技術,都使用了Java語言運行期類加載的特性。git

Java虛擬機類加載分爲5個過程:加載、驗證、準備、解析和初始化。github

在加載階段,虛擬機須要完成如下3件事情:數組

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

而加載階段的第一步經過一個類的全限定名來獲取定義此類的二進制字節流被放到了JVM外部去實現,這就給了咱們決定如何去獲取所須要類的權利。實現這個動做的代碼模塊咱們稱爲ClassLoader。安全

雙親委派模型

不管是JVM仍是Android,它們在加載類的時候都遵循雙親委派模型。雙親委派模型是這樣的,每個類加載器都有一個父加載器,若是某個類加載器收到了加載類的請求,它不會本身處理,而是交給父加載處理。每一層的類加載器都會這樣向上傳遞,所以全部的類加載請求都會到達頂層的根加載器。只有父加載器不能處理加載請求時,子加載器纔會嘗試處理。具體代碼以下:網絡

public abstract class ClassLoader {

	private ClassLoader parent;

	protected ClassLoader(ClassLoader parentLoader) {
        this.parent = parentLoader;
    }

    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }

    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
      	//查找類是否已經加載過
        Class<?> clazz = findLoadedClass(className);
	//類沒有加載過
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
              	//交給父加載器處理
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
	    //父加載器不能處理加載請求
            if (clazz == null) {
                try {
                    //當前類加載器處理加載請求
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
        return clazz;
    }

    protected Class<?> findClass(String className) throws ClassNotFoundException {
        throw new ClassNotFoundException(className);
    }
}
複製代碼

這段代碼的解釋以下:數據結構

  1. ClassLoader是一個抽象類。它的構造方法須要傳入一個父加載器。
  2. ClassLoader提供一個public的loadClass方法,這個方法的做用就是根據類的全限定名加載類,它的內部調用了protected的loadClass。
  3. protected的loadClass是雙親委派模型的核心。先檢查是否已經加載過,若沒有加載過則調用父加載器加載,若父加載器加載失敗,則調用本身的findClas()方法加載。

ClassLoader爲咱們提供了兩個protected的方法loadeClass(string className, boolean resolve)和findClass(String className)。你也許會有疑惑,loadeClass(string className, boolean resolve)爲何要聲明爲protected的呢,這樣子類豈不是能夠重寫這個方法從而繞過了雙親委派模型。其實,這是因爲歷史緣由形成的。在Java初期,開發JDK的大腦殼們並無提供findClass()方法,雙親委派模型須要開發者本身去維護。Java 1.2時,這些大腦殼們爲了重構了ClassLoader,纔有了findClass()方法,可是爲了兼容以前的版本,loadClass()方法保留了protected聲明。因此,爲了安全起見,咱們仍是老老實實的重寫findClass()方法吧。app

Android中的ClassLoader

爲了可以加載dex/apk文件,Android從新定義了一系列的ClassLoader。其中的PathClassLoader和DexClassLoader是本文分析的對象。框架

PathClassLoader

PathClassLoader是什麼呢?要弄清楚這個問題須要對app的啓動流程有個簡單的認識。首先,apk在安裝成功後會被存儲在data/app/的目錄下。而後,app啓動時,系統會去data/app/目錄下找到相應的apk加載到內存中。而這個加載動做就是經過PathClassLoader完成的。所以,PathClassLoader只能加載系統中已經安裝過的apk,對應到插件化技術中就是加載宿主apk。

/** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). */
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);
    }
}
複製代碼

上面這段代碼是PathClassLoader的所有代碼了,咱們能夠看到它單純的繼承了BaseDexClassLoader。關於BaseDexClassLoader咱們一會再說。

DexClassLoader

DexClassLoader是一種容許app運行期間加載外部jar/apk文件的加載器。所以咱們用它來加載插件。

/** * A class loader that loads classes from {@code .jar} and {@code .apk} files * containing a {@code classes.dex} entry. This can be used to execute code not * installed as part of an application. * * <p>This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create * such a directory: <pre> {@code * File dexOutputDir = context.getCodeCacheDir(); * }</pre> * * <p><strong>Do not cache optimized classes on external storage.</strong> * External storage does not provide access controls necessary to protect your * application from code injection attacks. */
public class DexClassLoader extends BaseDexClassLoader {
  
    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
複製代碼

和PathClassLoader同樣,DexClassLoader也只是繼承了BaseDexClassLoader。看來,只要弄清楚了BaseDexClassLoader就能理解PathClassLoader和DexClassLoader的加載機制了。

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            throw new ClassNotFoundException();
        }
        return c;
    }
}
複製代碼

上面這段代碼是BaseDexClassLoader加載類的關鍵代碼,它仍是很是簡單的。

構造方法有四個參數,含義以下:

  • dexPath: 包含目標類或資源的apk/jar列表;當有多個路徑則採用:分割。
  • optimizedDirectory: 優化後dex文件存在的目錄, 能夠爲null。
  • libraryPath: native庫所在路徑列表;當有多個路徑則採用:分割。
  • Parent:父類的類加載器。

在構造方法中經過this、dexpath、libraryPath、optimizedDirectory生成了一個DexPathList對象,並保存在pathList中。

重寫的findClass()方法中,將加載類的具體邏輯交給了pathList對象。

咱們接着瞭解DexPathList。

public class DexPathList{

    private final Element[] dexElements;
    private final ClassLoader definingContext;
  
    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
        this.definingContext = definingContext;
        // save dexPath for BaseDexClassLoader
        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory);
    }

    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;
                }
            }
        }
        return null;
    }
}    
複製代碼

DexPathList類的代碼很長,上面這段只列出了咱們須要關心的,它的意思就是在new DexPathList時,會經過構造方法中的dexPath,optimizedDirectory生成一個Element[]數組,並保存在dexElements中。在真正加載類的findClass()方法中,遍歷dexElements,經過Element的loadClassBinaryName加載Class。這裏請記住dexElements,由於它在後文中很重要。

源碼分析到這裏就結束了,由於插件化不須要更深的知識了。若是你想了解Android ClassLoader整個加載流程,能夠學習這篇文章Android類加載器ClassLoader

Android ClassLoader機制在插件化中的應用

咱們在Android插件化開篇中提到過,插件化是將一個apk拆分紅一個宿主和多個插件的技術。那必然有如下三個問題須要考慮:

  1. 插件如何加載宿主中的類。
  2. 宿主如何加載插件中的類。
  3. 一個插件如何加載其它插件中的類。

第一個問題比較簡單,咱們只須要在構造插件DexClassLoader時,把宿主PathClassLoader做爲parent傳遞進去(雙親委託模型)。

第二個問題比較複雜,由於宿主PathClassLoade沒辦法直接拿到插件的信息。那有沒有辦法在運行期間動態向宿主PathClassLoader添加插件apk信息呢?答案是確定的,它要靠上文提到的dexElements完成。咱們在原理部分分析了宿主PathClassLoader加載類的動做其實是遍歷DexPathList的dexElements完成的,若是咱們將插件DexClassLoader中的dexElements添加到宿主PathClassLoader中去,是否是宿主PathClassLoader也有了插件的信息了呢。

因爲雙親模型,第二個問題解決了,第三個問題也天然就解決了。

具體的代碼邏輯以下:

protected ClassLoader createClassLoader(Context context, File apk, ClassLoader parent) throws Exception {

        File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
        String dexOutputPath = dexOutputDir.getAbsolutePath();
        File nativeLibDir = getDir(context, Constants.NATIVE_DIR);
        DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, nativeLibDir.getAbsolutePath(), parent);
       
        DexUtil.insertDex(loader, parent);
        return loader;
    }
複製代碼
public class DexUtil {
    //將dexClassLoader的dexElements添加到baseClassLoader的dexElements中去。
    public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader) throws Exception {
        Object baseDexElements = getDexElements(getPathList(baseClassLoader));
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        Object allDexElements = combineArray(baseDexElements, newDexElements);
        Object pathList = getPathList(baseClassLoader);
        Reflector.with(pathList).field("dexElements").set(allDexElements);
    }
    //經過反射獲取dexElements
    private static Object getDexElements(Object pathList) throws Exception {
        return Reflector.with(pathList).field("dexElements").get();
    }
    //經過反射獲取PathClassLoader/DexClassLoader中的pathList
    private static Object getPathList(ClassLoader baseDexClassLoader) throws Exception {
        return Reflector.with(baseDexClassLoader).field("pathList").get();
    }
    //合併數組
    private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int secondArrayLength = Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, firstArrayLength + secondArrayLength);
        System.arraycopy(firstArray, 0, result, 0, firstArrayLength);
        System.arraycopy(secondArray, 0, result, firstArrayLength, secondArrayLength);
        return result;
    }
}
複製代碼

Android插件化開篇中我說過,每一篇文章的最後都會是一個Demo,這些Demo串聯起來就是一個插件化框架,因此我在Github上建了一個項目VirtualApkLike,Demo都會以不一樣的分支放到這裏。本文的Demo在classloader分支上。

相關文章
相關標籤/搜索