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

大話(huà)插件化系列目錄
插件化(一) 插件化思想與類(lèi)加載
插件化(二) 插件化Activity的啟動(dòng)
插件化(三) 插件資源加載

最開(kāi)始的起源:插件化技術(shù)最初源于免安裝運(yùn)行 apk 的想法地梨。

免安裝的 apk 我們稱(chēng)它為 插件
支持插件的 app 我們稱(chēng)它為 宿主

免安裝的 apk 我們稱(chēng)它為 插件
支持插件的 app 我們稱(chēng)它為 宿主

插件話(huà)解決的問(wèn)題

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

由于維護(hù)成本高,技術(shù)難點(diǎn)大涕侈,大公司一線(xiàn)公司用的比較多沪停,而且兼容問(wèn)題比較多,所以維護(hù)起來(lái)難點(diǎn)大裳涛。

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

組件化開(kāi)發(fā)就是將一個(gè)app分成多個(gè)模塊木张,每個(gè)模塊都是一個(gè)組件,開(kāi)發(fā)的 過(guò)程中我們可以讓這些組件相互依賴(lài)或者單獨(dú)調(diào)試部分組件等端三,但是最終發(fā) 布的時(shí)候是將這些組件合并統(tǒng)一成一個(gè)apk舷礼,這就是組件化開(kāi)發(fā)。
再具體一些郊闯,就是 組件化分模塊縱向依賴(lài)公共庫(kù)妻献,橫向彼此之間沒(méi)有直接依賴(lài)關(guān)系蛛株。

插件化開(kāi)發(fā)和組件化略有不同,插件化開(kāi)發(fā)是將整個(gè)app拆分成多個(gè)模塊育拨, 這些模塊包括一個(gè)宿主和多個(gè)插件谨履,每個(gè)模塊都是一個(gè)apk,最終打包的時(shí) 候宿主apk和插件apk分開(kāi)打包熬丧。

模塊化笋粟,組件化和模塊化似乎類(lèi)似。但是目的不一樣析蝴,模塊話(huà)是業(yè)務(wù)為主害捕,用業(yè)務(wù)劃分模塊,但是傳統(tǒng)的這種做法導(dǎo)致多個(gè)業(yè)務(wù)關(guān)聯(lián)耦合闷畸。

插件話(huà)的實(shí)現(xiàn)思路尝盼,面臨的幾個(gè)難題

  1. 如何加載插件的類(lèi)?
  2. 如何啟動(dòng)插件的四大組件?
  3. 如何加載插件的資源?

可以做的功能,換膚腾啥,熱修復(fù)东涡,多開(kāi),ABTest

類(lèi)聲明周期簡(jiǎn)單看

我們抽象一個(gè)類(lèi)Person
我們抽象一個(gè)類(lèi)Car
這些都是類(lèi)Class
我們的Class也是類(lèi)Class

加載------> 驗(yàn)證 ----->  準(zhǔn)備------> 解析
                                    |->初始化->使用->卸載

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

為什么我們說(shuō)反射會(huì)有一定的降低效率

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

ClassLoader 繼承的關(guān)系

ClassLoader 繼承的關(guān)系.png

PathClassLoader & DexClassLoader

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

這就是兼容問(wèn)題琼富,以后有沒(méi)有,每次更新都要查看庄新,所以說(shuō)維護(hù)成本高

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

寫(xiě)一個(gè)測(cè)試代碼:

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

        //pathClassLoader 和 BootClassLoader 分別加載什么類(lèi)
        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類(lèi)型的對(duì)象)鞠眉,BootClassLoader 沒(méi)有parent

PathClassLoader --- 應(yīng)用的 類(lèi) -- 第三方庫(kù)
BootClassLoader --- SDK的類(lèi)

Activity 是SDK 而不是FrameWork,而AppCompatActivity 是依賴(lài)庫(kù)中的
類(lèi)似Glide 都是第三方集成的依賴(lài)。

測(cè)試加載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:啟動(dòng)插件中方法");
    }
}

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)加載择诈,如果沒(méi)有委派雙親去加載械蹋,如果沒(méi)有加載出來(lái)那么在自己查找

 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.避免重復(fù)加載
2.安全考慮,不能攥改

雙親委派.png

Hook 點(diǎn)

查找 Hook 反射 啟動(dòng)插件的類(lèi)

一個(gè)dexFile -> 對(duì)應(yīng)一個(gè)dex文件
Element --> 對(duì)應(yīng) dexFile 而 一個(gè)APK-> 多個(gè)dex文件

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

關(guān)注這些類(lèi)的流程

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

因?yàn)?宿主的MainActivity 在 宿主 的 dexElements 里面

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

合并.png

ps:熱修復(fù)原理類(lèi)似羞芍,就是更換加載順序哗戈,把修復(fù)好的elements放在未曾修復(fù)的前面加載,就不會(huì)在加載一個(gè)錯(cuò)誤的了

目標(biāo):dexElements -- DexPathList類(lèi)的對(duì)象 -- BaseDexClassLoader的對(duì)象荷科,類(lèi)加載器

獲取的是宿主的類(lèi)加載器 --- 反射 dexElements 宿主

獲取的是插件的類(lèi)加載器 --- 反射 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.合并兩個(gè)dexElements
         * 4.將新的dexElements 賦值到 宿主dexElements
         *
         * 目標(biāo):dexElements  -- DexPathList類(lèi)的對(duì)象 -- BaseDexClassLoader的對(duì)象唯咬,類(lèi)加載器
         *
         * 獲取的是宿主的類(lèi)加載器  --- 反射 dexElements  宿主
         *
         * 獲取的是插件的類(lèi)加載器  --- 反射 dexElements  插件
         */

        try {
            Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = clazz.getDeclaredField("pathList");// 只和類(lèi)有關(guān)和對(duì)象無(wú)關(guān)
            pathListField.setAccessible(true);

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

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


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

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


            //將新的dexElements 賦值到 宿主dexElements
            // 不能直接Object[] obj = new Object[] 因?yàn)槲覀円裲bj放到反射的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);

寫(xiě)測(cè)試方法

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

各大插件的介紹和對(duì)比

我們?cè)谶x擇開(kāi)源框架的時(shí)候胆胰,需要根據(jù)自身的需求來(lái)奈附,如果加載的插件不需要和宿主有任何耦合,也無(wú)須和宿主進(jìn)行通信煮剧,比如加載第三方 App,那么推薦使用 RePlugin将鸵,其他的情況推薦使用 VirtualApk勉盅。

特性 DynamicAPK dynamic-load-apk Small DroidPlugin RePlugin VirtualAPK
支持四大組件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持 全支持
組件無(wú)需在宿主manifest中預(yù)注冊(cè) ×
插件可以依賴(lài)宿主 ×
支持PendingIntent × × ×
Android特性支持 大部分 大部分 大部分 幾乎全部 幾乎全部 幾乎全部
兼容性適配 一般 一般 中等
插件構(gòu)建 部署aapt 無(wú) Gradle插件 無(wú) Gradle插件 Gradle插件
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者顶掉。
  • 序言:七十年代末草娜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子痒筒,更是在濱河造成了極大的恐慌宰闰,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件簿透,死亡現(xiàn)場(chǎng)離奇詭異移袍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)老充,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)葡盗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人啡浊,你說(shuō)我怎么就攤上這事觅够。” “怎么了巷嚣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵喘先,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我廷粒,道長(zhǎng)窘拯,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任评雌,我火速辦了婚禮树枫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘景东。我一直安慰自己砂轻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布斤吐。 她就那樣靜靜地躺著搔涝,像睡著了一般厨喂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庄呈,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天蜕煌,我揣著相機(jī)與錄音,去河邊找鬼诬留。 笑死斜纪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的文兑。 我是一名探鬼主播盒刚,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼绿贞!你這毒婦竟也來(lái)了因块?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤籍铁,失蹤者是張志新(化名)和其女友劉穎涡上,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拒名,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吩愧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了增显。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耻警。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甸怕,靈堂內(nèi)的尸體忽然破棺而出甘穿,到底是詐尸還是另有隱情,我是刑警寧澤梢杭,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布温兼,位于F島的核電站,受9級(jí)特大地震影響武契,放射性物質(zhì)發(fā)生泄漏募判。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一咒唆、第九天 我趴在偏房一處隱蔽的房頂上張望届垫。 院中可真熱鬧,春花似錦全释、人聲如沸装处。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)妄迁。三九已至寝蹈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間登淘,已是汗流浹背箫老。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黔州,地道東北人耍鬓。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像流妻,于是被迫代替她去往敵國(guó)和親界斜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容