插件化(一)插件化思想與類加載

大話插件化系列目錄
插件化(一) 插件化思想與類加載
插件化(二) 插件化Activity的啟動
插件化(三) 插件資源加載

最開始的起源:插件化技術(shù)最初源于免安裝運行 apk 的想法。

免安裝的 apk 我們稱它為 插件
支持插件的 app 我們稱它為 宿主

免安裝的 apk 我們稱它為 插件
支持插件的 app 我們稱它為 宿主

插件話解決的問題

  1. APP的功能模塊越來越多,體積越來越大
  2. 模塊之間的耦合度高,協(xié)同開發(fā)溝通成本越來越大
  3. 方法數(shù)目可能超過65535,APP占用的內(nèi)存過大
  4. 應用之間的互相調(diào)用

由于維護成本高,技術(shù)難點大,大公司一線公司用的比較多,而且兼容問題比較多,所以維護起來難點大。

插件話與組件化, 模塊化的區(qū)別

組件化開發(fā)就是將一個app分成多個模塊,每個模塊都是一個組件,開發(fā)的 過程中我們可以讓這些組件相互依賴或者單獨調(diào)試部分組件等,但是最終發(fā) 布的時候是將這些組件合并統(tǒng)一成一個apk,這就是組件化開發(fā)。
再具體一些,就是 組件化分模塊縱向依賴公共庫,橫向彼此之間沒有直接依賴關系。

插件化開發(fā)和組件化略有不同,插件化開發(fā)是將整個app拆分成多個模塊, 這些模塊包括一個宿主和多個插件,每個模塊都是一個apk,最終打包的時 候宿主apk和插件apk分開打包。

模塊化,組件化和模塊化似乎類似。但是目的不一樣,模塊話是業(yè)務為主,用業(yè)務劃分模塊,但是傳統(tǒng)的這種做法導致多個業(yè)務關聯(lián)耦合。

插件話的實現(xiàn)思路,面臨的幾個難題

  1. 如何加載插件的類?
  2. 如何啟動插件的四大組件?
  3. 如何加載插件的資源?

可以做的功能,換膚,熱修復,多開,ABTest

類聲明周期簡單看

我們抽象一個類Person
我們抽象一個類Car
這些都是類Class
我們的Class也是類Class

加載------> 驗證 ----->  準備------> 解析
                                    |->初始化->使用->卸載

加載階段,虛擬機做三件事:
1.通過一個類的全限定名來獲取定義此類的二 進制字節(jié)流。
2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為 方法區(qū)域的運行時數(shù)據(jù)結(jié)構(gòu)。
3.在Java堆中生成一個代表這個類的Class對象, 作為方法區(qū)域數(shù)據(jù)的訪問入口

為什么我們說反射會有一定的降低效率

  1. 產(chǎn)生大量的臨時對象
  2. 檢查可見性
  3. 會生成字節(jié)碼 --- 沒有優(yōu)化
  4. 類型轉(zhuǎn)換

ClassLoader 繼承的關系

ClassLoader 繼承的關系.png

PathClassLoader & DexClassLoader

在8.0(API 26)之前,它們二者的唯一區(qū)別是 第二個參數(shù) optimizedDirectory,這個參數(shù)的意 思是生成的 odex(優(yōu)化的dex)存放的路徑。
在8.0(API 26)及之后,二者就完全一樣了。
高版本合并了,所以區(qū)別不大了

這就是兼容問題,以后有沒有,每次更新都要查看,所以說維護成本高

public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
package dalvik.system;

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

寫一個測試代碼:

private void printClassLoader(){
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            Log.e("zcw_plugin", "classLoader:" + classLoader);
            classLoader = classLoader.getParent();
        }

        //pathClassLoader 和 BootClassLoader 分別加載什么類
        Log.e("zcw_plugin", "Activity 的 classLoader:" + Activity.class.getClassLoader());
        Log.e("zcw_plugin", "Activity 的 classLoader:" + AppCompatActivity.class.getClassLoader());

    }

打印

2020-11-25 12:18:01.911 7566-7566/top.zcwfeng.plugin E/zcw_plugin: classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/base.apk"],nativeLibraryDirectories=[/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/lib/x86, /system/lib]]]
2020-11-25 12:18:01.911 7566-7566/top.zcwfeng.plugin E/zcw_plugin: classLoader:java.lang.BootClassLoader@e9e6661
2020-11-25 12:18:01.911 7566-7566/top.zcwfeng.plugin E/zcw_plugin: Activity 的 classLoader:java.lang.BootClassLoader@e9e6661
2020-11-25 12:18:01.912 7566-7566/top.zcwfeng.plugin E/zcw_plugin: Activity 的 classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/base.apk"],nativeLibraryDirectories=[/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/lib/x86, /system/lib]]]

PathClassLoader --》 parent(ClassLoader類型的對象),BootClassLoader 沒有parent

PathClassLoader --- 應用的 類 -- 第三方庫
BootClassLoader --- SDK的類

Activity 是SDK 而不是FrameWork,而AppCompatActivity 是依賴庫中的
類似Glide 都是第三方集成的依賴。

測試加載dex

dex 的文件生成命令

dx --dex --output=output.dex input.class

dx --dex --output=test.dex top/zcwfeng/plugin/Test.class 
----------
source class

package top.zcwfeng.plugin;

import android.util.Log;

public class Test {
    public Test() {
    }

    public static void print() {
        Log.e("zcw_plugin", "print:啟動插件中方法");
    }
}

load dex

private void testLoadDex(){
        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
                MainActivity.this.getCacheDir().getAbsolutePath(),
                null,
                MainActivity.this.getClassLoader());
        try {
            Class<?> clazz = dexClassLoader.loadClass("top.zcwfeng.plugin.Test");
            Method clazzMethod = clazz.getMethod("print");
            clazzMethod.invoke(null);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

ClassLoader.Java 核心,雙親委派
先判斷是否已經(jīng)加載,如果沒有委派雙親去加載,如果沒有加載出來那么在自己查找

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

作用
1.避免重復加載
2.安全考慮,不能攥改

雙親委派.png

Hook 點

查找 Hook 反射 啟動插件的類

一個dexFile -> 對應一個dex文件
Element --> 對應 dexFile 而 一個APK-> 多個dex文件

Elements[] dexElements ---> 一個app的所有class文件都在dexElements 里面

關注這些類的流程

ClassLoader----DexPathList---Element----DexFile----BootClassLoader---VMClassLoader----Class

因為 宿主的MainActivity 在 宿主 的 dexElements 里面

1.獲取宿主dexElements
2.獲取插件dexElements
3.合并兩個dexElements
4.將新的dexElements 賦值到 宿主dexElements

合并.png

ps:熱修復原理類似,就是更換加載順序,把修復好的elements放在未曾修復的前面加載,就不會在加載一個錯誤的了

目標:dexElements -- DexPathList類的對象 -- BaseDexClassLoader的對象,類加載器

獲取的是宿主的類加載器 --- 反射 dexElements 宿主

獲取的是插件的類加載器 --- 反射 dexElements 插件

public
class LoadUtil {
    private final static String apkPath = "/sdcard/plugin-debug.apk";

    public static void load(Context context) {
        /**
         * 宿主dexElements = 宿主dexElements + 插件dexElements
         *
         * 1.獲取宿主dexElements
         * 2.獲取插件dexElements
         * 3.合并兩個dexElements
         * 4.將新的dexElements 賦值到 宿主dexElements
         *
         * 目標:dexElements  -- DexPathList類的對象 -- BaseDexClassLoader的對象,類加載器
         *
         * 獲取的是宿主的類加載器  --- 反射 dexElements  宿主
         *
         * 獲取的是插件的類加載器  --- 反射 dexElements  插件
         */

        try {
            Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = clazz.getDeclaredField("pathList");// 只和類有關和對象無關
            pathListField.setAccessible(true);

            Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            // 宿主的類加載器
            ClassLoader pathClassLoader = context.getClassLoader();
            // DexPathList 類對象
            Object hostPathList = pathListField.get(pathClassLoader);
            // 宿主的dexElements
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);


            // plugin的類加載器
            ClassLoader dexClassLoader = new DexClassLoader(apkPath,
                    context.getCacheDir().getAbsolutePath(),
                    null
                    , pathClassLoader);//parent 考慮適配問題,不要傳null

            // DexPathList 類對象
            Object pluginPathList = pathListField.get(dexClassLoader);
            // plugin的dexElements
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);


            //將新的dexElements 賦值到 宿主dexElements
            // 不能直接Object[] obj = new Object[] 因為我們要把obj放到反射的elements里面去,所以不行
            Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginDexElements.length);

            System.arraycopy(hostDexElements, 0, newDexElements, 0,
                    hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, newDexElements, hostDexElements.length,
                    pluginDexElements.length);

            //賦值
            dexElementsField.set(hostPathList, newDexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

加載 apk插件在application

        LoadUtil.load(this);

寫測試方法

private void testLoadDex(){
        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
                MainActivity.this.getCacheDir().getAbsolutePath(),
                null,
                MainActivity.this.getClassLoader());
        try {
            Class<?> clazz = dexClassLoader.loadClass("top.zcwfeng.plugin.Test");
            Method clazzMethod = clazz.getMethod("print");
            clazzMethod.invoke(null);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

各大插件的介紹和對比

我們在選擇開源框架的時候,需要根據(jù)自身的需求來,如果加載的插件不需要和宿主有任何耦合,也無須和宿主進行通信,比如加載第三方 App,那么推薦使用 RePlugin,其他的情況推薦使用 VirtualApk。

特性 DynamicAPK dynamic-load-apk Small DroidPlugin RePlugin VirtualAPK
支持四大組件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持 全支持
組件無需在宿主manifest中預注冊 ×
插件可以依賴宿主 ×
支持PendingIntent × × ×
Android特性支持 大部分 大部分 大部分 幾乎全部 幾乎全部 幾乎全部
兼容性適配 一般 一般 中等
插件構(gòu)建 部署aapt Gradle插件 Gradle插件 Gradle插件
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容