現(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)勢有:
- 極其靈活:主程序無需升級(無需在Manifest中預(yù)埋組件),即可支持新增的四大組件忆畅,甚至全新的插件
- 非常穩(wěn)定:Hook點僅有一處(ClassLoader)衡未,無任何Binder Hook!如此可做到其崩潰率僅為“萬分之一”家凯,并完美兼容市面上近乎所有的Android ROM
- 特性豐富:支持近乎所有在“單品”開發(fā)時的特性缓醋。包括靜態(tài)Receiver、Task-Affinity坑位绊诲、自定義Theme送粱、進(jìn)程坑位、AppCompat掂之、DataBinding等
- 易于集成:無論插件還是主程序抗俄,只需“數(shù)行”就能完成接入
管理成熟:擁有成熟穩(wěn)定的“插件管理方案”,支持插件安裝世舰、升級动雹、卸載、版本管理跟压,甚至包括進(jìn)程通訊胰蝠、協(xié)議版本、安全校驗等 - 數(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();
}
注意問題
- context上下文對象剖踊,注意用的是插件的context還是宿主的context
- 插件相關(guān)權(quán)限要提前在宿主中注冊庶弃。
- 利用反射來進(jìn)行資源的訪問
運行結(jié)果
自己寫了個簡單的Demo,就是宿主和插件之間四大組件的相互調(diào)用以及資源的相互獲取德澈。插件是外置插件歇攻。
源碼地址:https://github.com/LXD312569496/RePluginDemo
總結(jié)
通過這個簡單RePlugin的Demo,學(xué)會到了插件化的基本使用梆造,以及了解到了插件化的原理實現(xiàn)缴守。還有一點,就是RePlugin的源碼注釋寫得真是非常清晰明了,很詳細(xì)屡穗,值得學(xué)習(xí)贴捡。