Android插件化Step 2 - 插件加載機制

本文主要參考借鑒了weishu的文章,weishu在博客中講述了android插件化的一系列文章,寫的很好,只是他的代碼分析是基于Android6.0上的,Android8.0無法適用,所以本文針對這一部分做了修改。

在上一篇文章中講述了如何啟動沒有在AndroidManifest.xml中顯式聲明的Activity,通過Hook AMS和攔截ActivityThread中H類對于組件調度成功地繞過了AndroidMAnifest.xml的限制。但是我們啟動的『沒有在AndroidManifet.xml中顯式聲明』的Activity和宿主程序存在于同一個Apk中;通常情況下,插件均以獨立的文件存在甚至通過網絡獲取,這時候插件中的Activity應該怎么樣啟動?

系統通過ClassLoader加載了需要的Activity類并通過反射調用構造函數創建出了Activity對象。如果Activity組件存在于獨立于宿主程序的文件之中,系統的ClassLoader怎么知道去哪里加載呢?因此,如果不做額外的處理,插件中的Activity對象甚至都沒有辦法創建出來,談何啟動?

因此,要使存在于獨立文件或者網絡中的插件被成功啟動,首先就需要解決這個插件類加載的問題。

下文將圍繞此問題展開,完成『啟動沒有在AndroidManifest.xml中顯示聲明,并且存在于外部插件中的Activity』的任務。

ClassLoader類加載機制

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

Java的類加載是一個相對復雜的過程;它包括加載、驗證、準備、解析和初始化五個階段;對于開發者來說,可控性最強的是加載階段;加載階段主要完成三件事:

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

『通過一個類的全限定名獲取描述此類的二進制字節流』這個過程被抽象出來,就是Java的類加載器模塊,也即JDK中ClassLoader API。Android Framework提供了DexClassLoader這個類,簡化了『通過一個類的全限定名獲取描述次類的二進制字節流』這個過程;我們只需要告訴DexClassLoader一個dex文件或者apk文件的路徑就能完成類的加載。因此本文的內容用一句話就可以概括:
將插件的dex或者apk文件告訴『合適的』DexClassLoader,借助它完成插件類的加載。
不熟悉的可以看下這個文章classloader.

具體實現

明確了目的其實方式也很簡單。就是將插件的dex或者apk加入宿主classloader的路徑中,這樣宿主開啟插件應用時就會順著路徑直接找到插件的apk然后加載啟動。(這里講了大概原理,具體分析可以移步至weishu的博客,下有鏈接。)

public final class BaseDexClassLoaderHookHelper {

    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 獲取 BaseDexClassLoader : pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");

        // 獲取 PathList: Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");

        // Element 類型
        Class<?> elementClass = dexElements.getClass().getComponentType();

        // 創建一個數組, 用來替換原始的數組
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 構造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 這個構造函數
        Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
        Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
        Object o = RefInvoke.createObject(elementClass, p1, v1);

        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements復制進去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那個element復制進去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

        // 替換
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }
}

注意其中的這么幾句話:

Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Object o = RefInvoke.createObject(elementClass, p1, v1);
Object[] toAddElementArray = new Object[] { o };

這幾句話中,通過反射執行了Element的帶有4個參數的構造函數,但不幸的是,在Android O以及之后的版本,這個帶有4個參數的構造函數就被廢棄了。
此外,在這個構造函數中使用到的DexFile這個類,也被廢棄了,對此Google給出的解釋是,只有Android系統可以使用DexFile,App層面不能使用它。
于是,我們不得不另辟蹊徑,通過執行DexPathList類的makeDexElements方法,來生成插件中的dex:

List<File> legalFiles = new ArrayList<>();
legalFiles.add(apkFile);

List<IOException> suppressedExceptions = new ArrayList<IOException>();

Class[] p1 = {List.class, File.class, List.class, ClassLoader.class};
Object[] v1 = {legalFiles, optDexFile, suppressedExceptions, cl};
Object[] toAddElementArray = (Object[])
RefInvoke.invokeStaticMethod("dalvik.system.DexPathList", "makeDexElements", p1, v1);

小結

  1. 默認情況下performLacunchActivity會使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加載所有的類;我們的思路是告訴宿主ClassLoader我們在哪,讓其幫助完成類加載的過程。
  2. 宿主程序的ClassLoader最終繼承自BaseDexClassLoader,BaseDexClassLoader通過DexPathList進行類的查找過程;而這個查找通過遍歷一個dexElements的數組完成;我們通過把插件dex添加進這個數組就讓宿主ClasLoader獲取了加載插件類的能力。

最后, 同學點個贊吧!!! 加個關注好么
Android 插件化原理解析——插件加載機制

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容