Android-插件化技術(shù)之我也來入個門-DexClassLoader加載apk,反射調(diào)用插件方法

最近完全投入Android開發(fā)一年左右了靶累,中間也是一直補知識岭妖。到現(xiàn)在晾匠,還是補了蠻多的姑食。 布局上用約束布局很爽褥紫,應(yīng)該沒啥大問題送朱。 負責(zé)的布局熙尉,rv多type用的多橘沥,另外阿里的Vlayout也有嘗試臀晃,還有一些其他框架觉渴,有看過一些三方框架源碼,貌似也是多布局的封裝徽惋,還蠻騷的樣子案淋。自定義View之前搞過,流程基本ok险绘,問題不會太大踢京。然后到了后面自己封裝了彈窗庫誉碴,新項目也用到了(近期彈窗計劃正在針對地區(qū)選擇進行封裝,封裝后正好下一個版本迭代用上)瓣距,另外Android公共組件庫正在考慮中黔帕,因為做了幾個項目,基本很多控件都是類似的配置蹈丸,而且有些還是很重復(fù)的操作成黄,所以打算再搞一個公共組件庫(當(dāng)然其中包括涉及到自定義View、方便用戶配置)逻杖。簡單回味下....

然后一方面小萌新再看一些源碼奋岁,一方面打算抽點時間再深入下其他方面,比如插件化弧腥、熱修復(fù)等厦取,想想還是蠻重要的勒!

插件化的原理相關(guān)介紹:

1. 通過DexClassLoader加載管搪。

2. 代理模式添加生命周期虾攻。

3. Hook思想跳過清單驗證。

好吧更鲁,先嘗試實踐下DexClassLoader加載吧霎箍,參考網(wǎng)友的操作我們來過一下流程! 后面就開始著手做一些較深入的分析澡为,順便結(jié)合相關(guān)官方資料來加深印象漂坏!

Tips: DexClassLoader.loadClass()加載后可以如下方式調(diào)用插件的方法

//通過反射調(diào)用插件的代碼

//通過接口調(diào)用插件的代碼(其中包括較為完善的面向切面編程調(diào)用插件的方法)

**A. **試試反射的方式:

1. 創(chuàng)建工程

image

2. 新建一個Module- plugin

image
image

3. 然后plugin模塊下新建一個被調(diào)用的方法,比如 PluginTest.java, 并提供如下操作

   public class PluginTest {
    private String feature = "不帥";

    public String getFeature() {
        return feature;
    }

    public void setFeature(String feature) {
        this.feature = feature;
    }
}

4. 然后打包這個模塊為apk

image
image

5. 將plugin下的apk拷貝到app模塊下的assets目錄下

image

6. 搞工具類將assets目錄下的plugin-debug.apk拷貝到應(yīng)用目錄下,比如/data/user/0/popeeee.hl.com.plugin/files/Download/下,這樣可以避免還需要動態(tài)申請存儲權(quán)限的問題

image

7. 然后就可以進行拷貝操作了喲媒至,成功后進行apk的裝載

import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import popeeee.hl.com.plugin.utils.FileUtil;
import popeeee.hl.com.plugin.utils.SystemUtils;

public class MainActivity extends AppCompatActivity {
    private String pluginApkName = "plugin-debug.apk";  ///< 插件apk名稱
    private String apkPath;         ///< apk存儲路徑
    private String apkDexPath;      ///< apk解壓dex的目錄顶别、和apk存放路徑為一個路徑
    private DexClassLoader dexClassLoader;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ///< 獲取apk準備存儲的應(yīng)用本地緩存路徑
        this.apkDexPath = SystemUtils.getCacheDirectory(this, Environment.DIRECTORY_DOWNLOADS).getPath();
        ///< 拷貝assets下的plugin-debug.apk到apkPath目錄并獲取實際路徑
        this.apkPath = FileUtil.copyFilesFromAssets(this, pluginApkName, apkDexPath);
        ///< 加載apk并獲取DexClassLoader對象
        this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
    }
}

8. 給當(dāng)前控件添加一個點擊事件,然后點擊通過DexClassLoader.loadClass()加載插件對應(yīng)的類拒啰,然后通過反射獲取對應(yīng)的方法進行調(diào)用驯绎, 之前關(guān)于反射的學(xué)習(xí)MonkeyLei:Android-自定義注解-控件注解

   /**
     * 默認hello world文本框添加點擊事件 android:onClick="CallPlugin"
     * @param view
     */
    public void CallPlugin(View view) {
        try {
            ///< 加載插件的類(插件的包名.類名)
            Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.PluginTest");

            ///< 獲取類的實例
            Object beanObject = mClass.newInstance();

            ///< 然后通過反射獲取對應(yīng)的方法
            Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
            setFeatureMethod.setAccessible(true);
            Method getFeatureMethod = mClass.getMethod("getFeature");
            getFeatureMethod.setAccessible(true);

            ///< 然后執(zhí)行對應(yīng)方法進行相關(guān)設(shè)置和獲取
            setFeatureMethod.invoke(beanObject, "丑的不行呀!");
            String feature = (String) getFeatureMethod.invoke(beanObject);

            ///< 然后本地進行一些提示等操作
            Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

9. 當(dāng)點擊hello world后就可以看見回調(diào)信息了呀谋旦。剩失。。

image

以上方式加載過程都ok册着。 不過很多人都是把拷貝apk放到如下地方進行調(diào)用其實拷貝很快的拴孤,不一定要放到這里?ContextWrapper類的源碼甲捏,ContextWrapper中有一個attachBaseContext()方法演熟,這個方法會將傳入的一個Context參數(shù)賦值給mBase對象,之后mBase對象就有值了司顿。

Application中在onCreate()方法里去初始化各種全局的變量數(shù)據(jù)是一種比較推薦的做法绽媒,但是如果你想把初始化的時間點提前到極致蚕冬,也可以去重寫attachBaseContext()方法,同時加載apk時進行一個簡單判斷:

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        ///< 獲取apk準備存儲的應(yīng)用本地緩存路徑
        this.apkDexPath = SystemUtils.getCacheDirectory(this, Environment.DIRECTORY_DOWNLOADS).getPath();
        ///< 拷貝assets下的plugin-debug.apk到apkPath目錄并獲取實際路徑
        this.apkPath = FileUtil.copyFilesFromAssets(this, pluginApkName, apkDexPath);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ///< 判斷apk是否存在
        File file = new File(apkPath);
        if (!file.exists()){
            Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        ///< 加載apk并獲取DexClassLoader對象是辕,如果有.so需要考慮第三個參數(shù)
        this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
    }

B. 上面的加載方法還是略顯復(fù)雜囤热,有點麻煩了,如果加載的對象可以直接轉(zhuǎn)換為PluginTest對象豈不是妙哉!

由于app模塊并沒有這個PluginTeset類获三,所以沒法這樣操作旁蔼,有個做法是,把插件的類復(fù)制一份到app模塊疙教,然后直接強制轉(zhuǎn)換即可棺聊!試試是可以滴了....

image

這樣也是沒問題的。但是這樣很麻煩呀贞谓,你想想限佩,一旦插件要加個什么東西都需要拷貝一份,太煩了裸弦。 所以我們需要一個公共庫祟同,宿主和插件都依賴它,然后由它提供相關(guān)的實體類接口理疙,這樣只要都繼承對應(yīng)接口即可晕城,維護起來也方便很多呢!

1. 新建一個插件庫(主要是與插件對應(yīng))

image
image

2. 新建實體類對應(yīng)的公共接口 PluginProvider.java

   public interface PluginProvider {
    String getFeature();

    void setFeature(String feature);
}
image

3. 宿主和插件都依賴該庫窖贤,修改插件實體類繼承自PluginProvider

image
image
  1. 重新打包插件apk砖顷,更新到assets目錄下替換之前的插件

5. 然后宿主調(diào)用插件方式做一下改變,只需要強轉(zhuǎn)為PluginProvider即可赃梧,不依賴于插件具體的實體類類型

            ///< 面向接口編程調(diào)用插件代碼
            PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
            pluginProvider.setFeature("不帥么?");

            ///< 然后本地進行一些提示等操作
            Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
image

然后就ojbk了滤蝠。

image

**C. **有時候我們希望通過回調(diào)的方式調(diào)用插件的方法,因為插件還要做很多事情才能回調(diào)給宿主(比如插件需要去下載皮膚主題資源授嘀,然后解壓校驗物咳,成功后才能通知宿主進行相關(guān)設(shè)置),此時我們就采用接口編程回調(diào)的方式實現(xiàn)粤攒∷回調(diào)我們經(jīng)常用啦囱持,問題不大哈...

1. 公共插件庫中我們定義一個回調(diào)接口夯接,并提供一個invokeCallBack(ICallBack callBack)方法. IDynamic.java

public interface IDynamic {
    void invokeCallBack(ICallBack callBack);
}

ICallBack.java

public interface ICallBack {
    void callback(PluginProvider pluginProvider);
}

PluginProvider.java

  public interface PluginProvider {
    String getFeature();

    void setFeature(String feature);
}

2. 然后插件模塊就可以新建一個Dynamic 繼承實現(xiàn)IDynamic的方法,給出回調(diào)(利用線程做一個模擬)

 import popeeee.hl.com.pluginlibrary.ICallBack;
import popeeee.hl.com.pluginlibrary.IDynamic;

public class Dynamic implements IDynamic {
    @Override
    public void invokeCallBack(final ICallBack callBack) {
                    ///< 操作獲取某些信息纷妆,然后回調(diào)給宿主
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                Thread.sleep(3);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                PluginTest pluginTest = new PluginTest();
                pluginTest.setFeature("我來自互聯(lián)網(wǎng)盔几,我標志了人類的一大進步!“呸掩幢,不要臉!");
                callBack.callback(pluginTest);
            }
        }).start();
    }
}

3. 然后宿主app此時不再加載對應(yīng)的實體類(因為你加載了實體類也只是自己設(shè)置逊拍,自己獲取信息上鞠,沒什么卵用!)芯丧。 此時我們加載Dynamic類芍阎,然后調(diào)用插件的invoke方法來請求網(wǎng)絡(luò)等操作獲取我們真實想要的數(shù)據(jù)....

記得重新打包plugin模塊的apk,更新下下

然后修改下加載實體類并且進行強制轉(zhuǎn)換

image
   /**
     * 默認hello world文本框添加點擊事件 android:onClick="CallPlugin"
     * @param view
     */
    public void CallPlugin(View view) {
        try {
            ///< 加載插件的類(插件的包名.類名)
            Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.Dynamic");

            /// 1\. 反射方式調(diào)用
//            ///< 獲取類的實例
//            Object beanObject = mClass.newInstance();
//
//            ///< 然后通過反射獲取對應(yīng)的方法
//            Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
//            setFeatureMethod.setAccessible(true);
//            Method getFeatureMethod = mClass.getMethod("getFeature");
//            getFeatureMethod.setAccessible(true);
//
//            ///< 然后執(zhí)行對應(yīng)方法進行相關(guān)設(shè)置和獲取
//            setFeatureMethod.invoke(beanObject, "丑的不行呀缨恒!");
//            String feature = (String) getFeatureMethod.invoke(beanObject);

//            ///< 然后本地進行一些提示等操作
//            Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();

//            /// 2\. 強制轉(zhuǎn)換對應(yīng)包含操作方法的對象
//            PluginTest pluginTest = (PluginTest) mClass.newInstance();
//            pluginTest.setFeature("丑的還可以呀2谴咸!");
//
//            ///< 然后本地進行一些提示等操作
//            Toast.makeText(this, pluginTest.getFeature(), Toast.LENGTH_SHORT).show();

//            ///< 面向接口編程調(diào)用插件代碼
//            PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
//            pluginProvider.setFeature("不帥么?");
//
//            ///< 然后本地進行一些提示等操作
//            Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();

            ///< 面向切面編程調(diào)用插件代碼
            IDynamic iDynamic = (IDynamic) mClass.newInstance();
            iDynamic.invokeCallBack(new ICallBack() {
                @Override
                public void callback(PluginProvider pluginProvider) {
                    Looper.prepare();
                    ///< 然后本地進行一些提示等操作
                    Toast.makeText(MainActivity.this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
                    Looper.loop();
                }
            });
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

這樣就可以了

image

到這里插件的入門算是有所了解,另外自己親自實踐了一把骗露,感覺還是不一樣的岭佳。另外還有插件的兩個入門點,一個是插件資源的加載萧锉,一個是插件的Activity的加載啟動珊随。這個兩個小萌新要后面再搞。

搞的前提:1. 小萌新要去了解資源加載相關(guān)的機制柿隙,原理叶洞,源碼的解讀 2. 同樣Activity的加載也是需要解讀一些源碼方可深入些。 另外如果對ClassLoader還在陌生的話优俘,有必要去看下官方api京办,做一個解讀了....

Demo下載地址還是貼下吧,萬一需要了 https://gitee.com/heyclock/doc/blob/master/PluginTest/PluginTest.zip

先到這帆焕,貼幾個我覺得不錯的文章惭婿,共勉之,一起加油, 很多東西還是要自己實踐...還得有自己理解叶雹!

Android插件化技術(shù)入門

Android插件化入門指南

Android插件化——資源加載

https://blog.csdn.net/liangfeng093/article/details/78120803

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末财饥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子折晦,更是在濱河造成了極大的恐慌钥星,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件满着,死亡現(xiàn)場離奇詭異谦炒,居然都是意外死亡,警方通過查閱死者的電腦和手機风喇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門宁改,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人魂莫,你說我怎么就攤上這事还蹲。” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵谜喊,是天一觀的道長潭兽。 經(jīng)常有香客問我,道長斗遏,這世上最難降的妖魔是什么山卦? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮诵次,結(jié)果婚禮上怒坯,老公的妹妹穿的比我還像新娘。我一直安慰自己藻懒,他們只是感情好剔猿,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嬉荆,像睡著了一般归敬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鄙早,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天汪茧,我揣著相機與錄音,去河邊找鬼限番。 笑死舱污,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的弥虐。 我是一名探鬼主播扩灯,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼霜瘪!你這毒婦竟也來了珠插?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤颖对,失蹤者是張志新(化名)和其女友劉穎捻撑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缤底,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡顾患,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了个唧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片江解。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坑鱼,靈堂內(nèi)的尸體忽然破棺而出膘流,到底是詐尸還是另有隱情,我是刑警寧澤鲁沥,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布呼股,位于F島的核電站,受9級特大地震影響画恰,放射性物質(zhì)發(fā)生泄漏彭谁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一允扇、第九天 我趴在偏房一處隱蔽的房頂上張望缠局。 院中可真熱鬧,春花似錦考润、人聲如沸狭园。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽唱矛。三九已至,卻和暖如春井辜,著一層夾襖步出監(jiān)牢的瞬間绎谦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工粥脚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留窃肠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓刷允,卻偏偏與公主長得像冤留,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子树灶,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355

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