RePlugin插件化框架的學(xué)習(xí)

現(xiàn)狀

最近在接觸插件化方面的技術(shù),學(xué)習(xí)后趕緊坐下筆記熄求,給入門的朋友看毕莱, 一起學(xué)習(xí),一起進(jìn)步偏友。
當(dāng)前比較熱門的插件化框架有下面幾個:

框架 優(yōu)點 缺點
dynamic-load-apk 1.插件無需安裝host即可吊起
2.支持R訪問插件資源
3.插件支持Activity和FragmentActivity
4.基本無反射調(diào)用
5.插件安裝后任可獨立運行
1.不支持Service和BroadcastReceiver
2.遷移成本蔬胯,需要修改插件,插件app需要繼承自proxyActivity
Droid Plugin 1.插件無需任何修改位他,可獨立安裝運行氛濒,也可以做插件運行
2.四大組件無需在Host程序注冊
3.超強隔離性,不同插件運行在不同的進(jìn)程中
4.資源完全隔離
5.實現(xiàn)進(jìn)程管理鹅髓,插件的空進(jìn)程會被及時回收舞竿,占用內(nèi)存低插件的靜態(tài)廣播會被當(dāng)作動態(tài)處理,如果插件沒有運行迈勋,靜態(tài)廣播永遠(yuǎn)不會觸發(fā)
6.API侵入性低
1.無法使用自定義資源的通知
2.無法注冊一些特殊Intent Filter的組件(四大組件)
3.對Native支持不好
DynamicAPK 1.遷移成本低(無需做任何activity/fragment/resource的proxy實現(xiàn))不使用代理來管理插件的activity/fragment的生命周期炬灭。修改后aapt會處理插件種的資源,R.java中的資源引用和普通Android工程沒有區(qū)別靡菇,開發(fā)者可以保持原有的開發(fā)規(guī)范
2.更加有利于并發(fā)開發(fā)
3.提升編譯速度
4.提升啟動速度重归。dex解壓、dexopt厦凤、加載耗時較長鼻吮,使用按需加載啟動時間過長
5.適合HotFix(代碼和資源)
6.按需下載和加載任意功能模塊(包含代碼和資源)
目前已停止維護(hù)
RePlugin 1.極其靈活:主程序無需升級(無需在Manifest中預(yù)埋組件),即可支持新增的四大組件较鼓,甚至全新的插件
2.非常穩(wěn)定:Hook點僅有一處(ClassLoader)椎木,無任何Binder Hook!如此可做到其崩潰率僅為“萬分之一”博烂,并完美兼容市面上近乎所有的Android ROM
3.特性豐富:支持近乎所有在“單品”開發(fā)時的特性香椎。包括靜態(tài)Receiver、Task-Affinity坑位禽篱、自定義Theme畜伐、進(jìn)程坑位、AppCompat躺率、DataBinding等
4.易于集成:無論插件還是主程序玛界,只需“數(shù)行”就能完成接入
5.管理成熟:擁有成熟穩(wěn)定的“插件管理方案”万矾,支持插件安裝、升級慎框、卸載良狈、版本管理,甚至包括進(jìn)程通訊笨枯、協(xié)議版本薪丁、安全校驗等
6.數(shù)億支撐:有360手機衛(wèi)士龐大的數(shù)億用戶做支撐,三年多的殘酷驗證猎醇,確保App用到的方案是最穩(wěn)定窥突、最適合使用的

介紹

下面我們主要介紹的就是RePlugin框架.中文文檔地址:https://github.com/Qihoo360/RePlugin/blob/dev/README_CN.md
官方對這個框架的介紹:
RePlugin是一套完整的硫嘶、穩(wěn)定的阻问、適合全面使用的,占坑類插件化方案沦疾,由360手機衛(wèi)士的RePlugin Team研發(fā)称近,也是業(yè)內(nèi)首個提出”全面插件化“(全面特性、全面兼容哮塞、全面使用)的方案刨秆。
其主要優(yōu)勢有:

  1. 極其靈活:主程序無需升級(無需在Manifest中預(yù)埋組件),即可支持新增的四大組件忆畅,甚至全新的插件
  2. 非常穩(wěn)定:Hook點僅有一處(ClassLoader)衡未,無任何Binder Hook!如此可做到其崩潰率僅為“萬分之一”家凯,并完美兼容市面上近乎所有的Android ROM
  3. 特性豐富:支持近乎所有在“單品”開發(fā)時的特性缓醋。包括靜態(tài)Receiver、Task-Affinity坑位绊诲、自定義Theme送粱、進(jìn)程坑位、AppCompat掂之、DataBinding等
  4. 易于集成:無論插件還是主程序抗俄,只需“數(shù)行”就能完成接入
    管理成熟:擁有成熟穩(wěn)定的“插件管理方案”,支持插件安裝世舰、升級动雹、卸載、版本管理跟压,甚至包括進(jìn)程通訊胰蝠、協(xié)議版本、安全校驗等
  5. 數(shù)億支撐:有360手機衛(wèi)士龐大的數(shù)億用戶做支撐,三年多的殘酷驗證姊氓,確保App用到的方案是最穩(wěn)定、最適合使用的

支持的特性:

特性 描述
組件 四大組件(含靜態(tài)Receiver)
升級無需改主程序Manifest 完美支持
Android特性 支持近乎所有(包括SO庫等)
TaskAffinity & 多進(jìn)程 支持(坑位方案)
插件類型 支持自帶插件(自識別)喷好、外置插件
插件間耦合 支持Binder翔横、Class Loader、資源等
進(jìn)程間通訊 支持同步梗搅、異步禾唁、Binder、廣播等
自定義Theme & AppComat 支持
DataBinding 支持
安全校驗 支持
資源方案 獨立資源 + Context傳遞(相對穩(wěn)定)
Android 版本 API Level 9+ (2.3及以上)

使用

一无切、添加依賴

項目目錄下的build.gradle文件:

dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'

        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.2.1'
        classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.2.1'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }

宿主目錄下的build.gradle文件

apply plugin: 'replugin-host-gradle'
/**
 * 配置項均為可選配置荡短,默認(rèn)無需添加
 * 更多可選配置項參見replugin-host-gradle的RepluginConfig類
 * 可更改配置項參見 自動生成RePluginHostConfig.java
 */
repluginHostConfig {
    /**
     * 是否使用 AppCompat 庫
     * 不需要個性化配置時,無需添加
     */
    useAppCompat = true
//    /**
//     * 背景不透明的坑的數(shù)量
//     * 不需要個性化配置時哆键,無需添加
//     */
//    countNotTranslucentStandard = 6
//    countNotTranslucentSingleTop = 2
//    countNotTranslucentSingleTask = 3
//    countNotTranslucentSingleInstance = 2
}

dependencies {
     ……………………………………………………………………………
    compile 'com.qihoo360.replugin:replugin-host-lib:2.2.1'
}

插件目錄下的build.gradle文件

apply plugin: 'replugin-plugin-gradle'

dependencies {
    ……………………………………………………………………………
    compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.1'
}

修改完上面的文件后掘托,點擊sync后,就可以開始實現(xiàn)插件化了籍嘹。

配置Application類

如果您的工程已有Application類闪盔,則可以將基類切換到RePluginApplication即可。然后可以通過自定義RePluginCallbacks類和RePluginEventCallbacks類來實現(xiàn)宿主針對RePlugin的自定義行為

public class MyApplication extends RePluginApplication{
    @Override
    public void onCreate() {
        super.onCreate();
        RePlugin.App.onCreate();
    }
    @Override
    protected RePluginConfig createConfig() {
        RePluginConfig c = new RePluginConfig();
        // 允許“插件使用宿主類”辱士。默認(rèn)為“關(guān)閉”
        c.setUseHostClassIfNotFound(true);
        // FIXME RePlugin默認(rèn)會對安裝的外置插件進(jìn)行簽名校驗泪掀,這里先關(guān)掉,避免調(diào)試時出現(xiàn)簽名錯誤
        c.setVerifySign(false);
        c.setPrintDetailLog(BuildConfig.DEBUG);
        c.setUseHostClassIfNotFound(true);
        // 針對“安裝失敗”等情況來做進(jìn)一步的事件處理
        c.setEventCallbacks(new HostEventCallbacks(this));
        c.setMoveFileWhenInstalling(true);
        // FIXME 若宿主為Release颂碘,則此處應(yīng)加上您認(rèn)為"合法"的插件的簽名异赫,例如,可以寫上"宿主"自己的头岔。
        // RePlugin.addCertSignature("AAAAAAAAA");

        return c;
    }
    @Override
    protected RePluginCallbacks createCallbacks() {
        return new HostCallbacks(this);
    }

}
/**
 * 宿主針對RePlugin的自定義行為
 */
public class HostCallbacks extends RePluginCallbacks {

    public HostCallbacks(Context context) {
        super(context);
    }

    @Override
    public boolean onLoadLargePluginForActivity(Context context, String plugin, Intent intent, int process) {
        return super.onLoadLargePluginForActivity(context, plugin, intent, process);
    }

    @Override
    public boolean onPluginNotExistsForActivity(final Context context, final String plugin, Intent intent, int process) {
        // FIXME 當(dāng)插件"沒有安裝"時觸發(fā)此邏輯塔拳,可打開您的"下載對話框"并開始下載。
        // FIXME 其中"intent"需傳遞到"對話框"內(nèi)切油,這樣可在下載完成后蝙斜,打開這個插件的Activity
        if (BuildConfig.DEBUG) {
            Log.d("morse", "onPluginNotExistsForActivity: Start download... p=" + plugin + "; i=" + intent);
        }
        return super.onPluginNotExistsForActivity(context, plugin, intent, process);
    }
}
public class HostEventCallbacks extends RePluginEventCallbacks {
    public HostEventCallbacks(Context context) {
        super(context);
    }

    @Override
    public void onInstallPluginSucceed(PluginInfo info) {
        Log.d("morse", "onInstallPluginSucceed: Failed! info=" + info);
        super.onInstallPluginSucceed(info);
    }

    @Override
    public void onInstallPluginFailed(String path, InstallResult code) {
        // FIXME 當(dāng)插件安裝失敗時觸發(fā)此邏輯。您可以在此處做“打點統(tǒng)計”澎胡,也可以針對安裝失敗情況做“特殊處理”
        // 大部分可以通過RePlugin.install的返回值來判斷是否成功
        Log.d("morse", "onInstallPluginFailed: Failed! path=" + path + "; r=" + code);
        super.onInstallPluginFailed(path, code);
    }

    @Override
    public void onStartActivityCompleted(String plugin, String activity, boolean result) {
        // FIXME 當(dāng)打開Activity成功時觸發(fā)此邏輯孕荠,可在這里做一些APM、打點統(tǒng)計等相關(guān)工作
        Log.d("morse", "onStartActivityCompleted: plugin=" + plugin + "\r\n result=" + result);
        super.onStartActivityCompleted(plugin, activity, result);
    }
}

安裝或者升級插件

思路:
1攻谁、判斷插件是否已經(jīng)安裝稚伍;
2、如果沒有安裝戚宦,檢測本地是否下載插件个曙;
3、沒有下載插件,需要先下載插件垦搬;
4呼寸、如果沒有安裝插件,需要安裝插件猴贰;

private void startRePlugin(String pluginName,String apkPath) {
        //安裝插件過程
        PluginInfo pluginInfo = RePlugin.getPluginInfo(pluginName);
        //插件文件对雪,只有存在就進(jìn)行安裝或者更新
        File file = new File(apkPath);
        //判斷是否已經(jīng)安裝插件
        if (pluginInfo == null) {
            //插件未安裝的情況
            if (!file.exists()) {
                Toast.makeText(HostActivity.this, "插件安裝失敗,插件文件不存在", Toast.LENGTH_SHORT).show();
            } else {
                //安裝插件
                PluginInfo pluginInfo1 = RePlugin.install(apkPath);
                if (pluginInfo1 == null) {
                    Toast.makeText(HostActivity.this, "插件安裝失敗米绕,安裝出錯", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(HostActivity.this, "插件安裝成功", Toast.LENGTH_SHORT).show();
                }
            }

        } else {
            //插件已安裝,是否需要升級瑟捣,判斷條件是file是否為空
            if (file.exists()) {
                PluginInfo pluginInfo1 = RePlugin.install(file.getAbsolutePath());
                if (pluginInfo1 == null) {
                    Toast.makeText(HostActivity.this, "插件升級失敗", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(HostActivity.this, "插件升級成功", Toast.LENGTH_SHORT).show();
                }
            } else {
                Toast.makeText(HostActivity.this, "插件已安裝", Toast.LENGTH_SHORT).show();
                RePlugin.preload(pluginInfo);
            }
        }
    }

宿主調(diào)用插件

打開插件的activity

可以直接調(diào)用Replugin.startActivity方式,然后傳入相應(yīng)的參數(shù)就可以了栅干,也可以通過forResult的方法進(jìn)行啟動迈套。有挺多個重載的方法可以調(diào)用,具體的源碼是位于RePlugin這個類中

    /**
     * 開啟一個插件的Activity <p>
     * 其中Intent的ComponentName的Key應(yīng)為插件名(而不是包名)碱鳞,可使用createIntent方法來創(chuàng)建Intent對象
     *
     * @param context Context對象
     * @param intent  要打開Activity的Intent桑李,其中ComponentName的Key必須為插件名
     * @return 插件Activity是否被成功打開?
     * FIXME 是否需要Exception來做窿给?
     * @see #createIntent(String, String)
     * @since 1.0.0
     */
    public static boolean startActivity(Context context, Intent intent) {
        // TODO 先用舊的開啟Activity方案芙扎,以后再優(yōu)化
        ComponentName cn = intent.getComponent();
        if (cn == null) {
            // TODO 需要支持Action方案
            return false;
        }
        String plugin = cn.getPackageName();
        String cls = cn.getClassName();
        return Factory.startActivityWithNoInjectCN(context, intent, plugin, cls, IPluginManager.PROCESS_AUTO);
    }

    /**
     * 通過 forResult 方式啟動一個插件的 Activity
     *
     * @param activity    源 Activity
     * @param intent      要打開 Activity 的 Intent,其中 ComponentName 的 Key 必須為插件名
     * @param requestCode 請求碼
     * @param options     附加的數(shù)據(jù)
     * @see #startActivityForResult(Activity, Intent, int, Bundle)
     * @since 2.1.3
     */
    public static boolean startActivityForResult(Activity activity, Intent intent, int requestCode, Bundle options) {
        return Factory.startActivityForResult(activity, intent, requestCode, options);
    }

綁定插件中的service

跟啟動插件中的activity方式差不多填大,具體的源碼是位于PluginServiceClient這個類中戒洼,下面是綁定service的方法:

  /**
     * 綁定插件服務(wù),獲取其AIDL允华。近似于Context.bindService
     *
     * @param context Context對象
     * @param intent  要打開的服務(wù)名圈浇。如何填寫請參見類的說明
     * @param sc      ServiceConnection對象(等同于系統(tǒng))
     * @param flags   flags對象。目前僅支持BIND_AUTO_CREATE標(biāo)志
     * @return 是否成功綁定服務(wù)靴寂。大于0表示成功
     * @see android.content.Context#bindService(Intent, ServiceConnection, int)
     */
    public static boolean bindService(Context context, Intent intent, ServiceConnection sc, int flags) {
        return bindService(context, intent, sc, flags, false);
    }

插件調(diào)用宿主組件

打開宿主的activity磷蜀,更加簡單。調(diào)用service也是一樣的道理百炬。

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.qihoo360.replugin.sample.host", "com.qihoo360.replugin.sample.host.MainActivity"));
context.startActivity(intent);
Intent intent1 = new Intent();
                intent1.setComponent(new ComponentName("com.example.asus.replugindemo",
                        "com.example.asus.replugindemo.HostService"));
                startService(intent1);

資源的互相獲取

因為插件apk與宿主apk不在一個apk內(nèi)褐隆,那么一些資源的訪問必然要通過反射進(jìn)行獲取。

宿主獲取插件資源
Context context = RePlugin.fetchContext("com.example.asus.plugin");
                //獲取插件中的圖片資源
                Class<?> c=null;
                try {
                    c=context.getClassLoader().loadClass("com.example.asus.plugin.R$drawable");
                    int drawableId= (int) c.getField("ic_face_black_24dp").get(null);
                    iv.setImageDrawable(context.getResources().getDrawable(drawableId));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
                //獲取插件中的字符串資源
                Class<?> c1=null;
                try {
                    c1=context.getClassLoader().loadClass("com.example.asus.plugin.R$string");
                    Field field=c1.getField("app_name");
                    int strId= (int) field.get(null);
                    tv.setText(context.getResources().getString(strId));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
插件獲取宿主資源
            //獲取宿主中的字符串資源
                Class<?> clazz = null;
                try {
                    clazz = RePlugin.getHostClassLoader().loadClass("com.example.asus.replugindemo.R$string");
                    Field field = clazz.getField("app_name");
                    int identifierID = (int) field.get(null);
                    tv.setText(RePlugin.getHostContext().getResources().getString(identifierID));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
                //獲取宿主中的圖片資源
                Class<?> c = null;
                try {
                    c = RePlugin.getHostClassLoader().loadClass("com.example.asus.replugindemo.R$drawable");
                    Field field = c.getField("ic_tag_faces_black_24dp");
                    int drawableId = (int) field.get(null);
                    Drawable drawable = RePlugin.getHostContext().getResources().getDrawable(drawableId);
                    iv.setImageDrawable(drawable);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

注意問題

  1. context上下文對象剖踊,注意用的是插件的context還是宿主的context
  2. 插件相關(guān)權(quán)限要提前在宿主中注冊庶弃。
  3. 利用反射來進(jìn)行資源的訪問

運行結(jié)果

自己寫了個簡單的Demo,就是宿主和插件之間四大組件的相互調(diào)用以及資源的相互獲取德澈。插件是外置插件歇攻。
源碼地址:https://github.com/LXD312569496/RePluginDemo

image.png

image.png

總結(jié)

通過這個簡單RePlugin的Demo,學(xué)會到了插件化的基本使用梆造,以及了解到了插件化的原理實現(xiàn)缴守。還有一點,就是RePlugin的源碼注釋寫得真是非常清晰明了,很詳細(xì)屡穗,值得學(xué)習(xí)贴捡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市村砂,隨后出現(xiàn)的幾起案子栈暇,更是在濱河造成了極大的恐慌,老刑警劉巖箍镜,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異煎源,居然都是意外死亡色迂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門手销,熙熙樓的掌柜王于貴愁眉苦臉地迎上來歇僧,“玉大人,你說我怎么就攤上這事锋拖≌┖罚” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵兽埃,是天一觀的道長。 經(jīng)常有香客問我舷夺,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任恒削,我火速辦了婚禮钓丰,結(jié)果婚禮上饶辙,老公的妹妹穿的比我還像新娘。我一直安慰自己斑粱,他們只是感情好弃揽,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著痕慢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娜庇。 梳的紋絲不亂的頭發(fā)上塔次,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音匕得,去河邊找鬼翠忠。 笑死态秧,一個胖子當(dāng)著我的面吹牛淫半,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播势告,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼瓮恭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绳姨?” 一聲冷哼從身側(cè)響起登澜,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎飘庄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跪削,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡谴仙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碾盐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晃跺。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖毫玖,靈堂內(nèi)的尸體忽然破棺而出掀虎,到底是詐尸還是另有隱情凌盯,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布涩盾,位于F島的核電站十气,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏春霍。R本人自食惡果不足惜砸西,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望址儒。 院中可真熱鬧芹枷,春花似錦、人聲如沸莲趣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喧伞。三九已至走芋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間潘鲫,已是汗流浹背翁逞。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留溉仑,地道東北人挖函。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像浊竟,于是被迫代替她去往敵國和親怨喘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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