30. 插件化實現方式-加載插件中的類

什麼是插件化

插件化就是以插件下發的到本地的方式,然後通過宿主apk加載插件apk以實現相應的功能的方法,很多大型項目都採用了插件化的方式,如支付寶,微信,甚至最近在研究廣點通廣告sdk的時候偶然發現,連廣點通廣告sdk都使用了插件化開發(雖然廣點通廣告的主流地位已被取代,但是不排除它的技術含量是最高的)

插件化的好處

  1. 通過插件下發的方式,減小安裝包體積
  2. 降低模塊耦合度,提高協同開發效率
  3. 緩解方法數目可能超過65535的風險
  4. 爲應用之間的互相調用提供方便

插件化與組件化

插件化開發是將整個app拆分成多個模塊, 這些模塊包括一個宿主和多個插件,每個模塊都是一個apk,最終打包的時 候宿主apk和插件apk分開打包。
組件化開發同樣是將一個app分成多個模塊,每個模塊都是一個組件,開發的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等,但是最終發 布的時候是將這些組件合併統一成一個apk

如何加載插件中的類

要知道怎麼加載類,首先要了解類加載器,其次還要了解雙親委託機制。

PathClassLoader和DexClassLoader的區別

安卓中加載器的繼承關係如上,在系統設計的初期,安卓系統考慮的是PathClassLoader用於加載正在運行中的apk,如你的app從啓動到執行各種功能,它的內部都是通過PathClassLoader加載;而如果你要加載一個沒有運行的apk中的類,安卓希望你使用DexClassLoader,雖然二者都是繼承自BaseDexClassLoader,並且區別不大,但這樣區分二者的功能也更加清晰明瞭。但是隨着版本的更新,兩者的區別逐漸被抹殺,已經到了可以相互替換的地步。在8.0(API 26)之前,它們二者的唯一區別是 第二個參數 optimizedDirectory,這個參數的意 思是生成的 odex(優化的dex)存放的路徑。在8.0(API 26)及之後,二者就完全一樣了

BootClassLoader

除特殊說明外,這些源碼都是api28
BootClassLoader 是 PathClassLoader 的 parent,這裏要注意 parent 與父類的區別。

ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
    System.out.println("classLoader = " + classLoader);
    classLoader = classLoader.getParent();
}
ClassLoader activityClassLoader = Activity.class.getClassLoader();
System.out.println("activityClassLoader = " + activityClassLoader);
類加載

從ClassLoader的loadClass方法跟下去,會找到BootClassLoader的findClass方法

    .....
    private final DexPathList pathList;
    .....
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        .....
        Class c = pathList.findClass(name, suppressedExceptions);
        .....
        return c;
    }

然後進入這裏,我們只要知道,一個Element對應一個dex文件即可,因爲一個app可能包含多個dex

    private Element[] dexElements
    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
        ......
        return null;
    }

可以看到class就是從這個dexElements數組中加載出來的。那麼我們可以考慮下,宿主apk要調用插件apk的class,是不是可以把插件apk的dexElements和宿主的dexElements合併呢?當然可以

開始合併dexElements並加載類

a.創建插件的 DexClassLoader 類加載器,然後通過反射獲取插件的 dexElements 值
b.獲取宿主的 PathClassLoader 類加載器,然後通過反射獲取宿主的 dexElements 值。
c.合併宿主的 dexElements 與 插件的 dexElements,生成新的 Element[]。
d.最後通過反射將新的 Element[] 賦值給宿主的 dexElements

        //3.獲取BaseDexClassLoader對象,宿主的classloader
        ClassLoader classLoader = context.getClassLoader();

        //2.獲取DexPathList對象,需要用到BaseDexClassLoader對象
        Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathListObj = pathListField.get(classLoader);

        //1.獲取dexElements對象(宿主的),需要用到DexPathList對象
        Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
        Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        Object[] dexElementsObj = (Object[]) dexElementsField.get(pathListObj);

        //4.獲取插件的dexElements對象,需要用到DexPathList對象,由上邊的123部反推,DexPathList對象需要插件
        //的BaseDexClassLoader對象,那麼我們需要構建一個ClassLoader去加載插件
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
                context.getCacheDir().getAbsolutePath(), null, classLoader);

        //5.插件的dexElements對象
        Object pluginPathListObj = pathListField.get(dexClassLoader);
        Object[] pluginDexElementObj = (Object[]) dexElementsField.get(pluginPathListObj);


        // 創建一個新數組
        Object[] newDexElements = (Object[]) Array.newInstance(dexElementsObj.getClass().getComponentType(),
                dexElementsObj.length + pluginDexElementObj.length);

        System.arraycopy(dexElementsObj, 0, newDexElements,
                0, dexElementsObj.length);
        System.arraycopy(pluginDexElementObj, 0, newDexElements,
                dexElementsObj.length, pluginDexElementObj.length);

        // 賦值
        dexElementsField.set(pathListObj, newDexElements);

那麼接下來你就可以通過反射調用到插件中的類了

Class<?> aClass = Class.forName("com.rzm.plugin.PluginClass1");
Method print = aClass.getDeclaredMethod("print");
Object o = aClass.newInstance();
print.invoke(o);
System.out.println("MainActivity o = " + o);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章