Android 插件化框架經過多年的發展已經出現很多成熟的方案。依然記得自己最早接觸的DL框架,在能夠讓APP不經過安裝就可以加載功能新模塊,別提有多興奮。再到之後的360的 DroidPlugin 等等,感謝開發者們無私的奉獻,讓大家受益匪淺。
接下來會有一個插件化系列的文章主要介紹一下當下一些插件化框架的設計思想和代碼邏輯,深入理解插件化開發。
Replugin 框架原理
Replugin 框架是360開源的一款插件化框架,其實現思路走的還是佔坑的思想,我們知道Android的組件需要在AndroidManifest.xml中註冊,佔坑的思想就是預先註冊坑位,然後用插件中的組件替換坑位註冊的組件欺騙系統從而實現組件的加載,這個和360 張勇的DroidPlugin思想是一樣的,兩者的一大區別就是 Replugin 宣傳的唯一 hook點。只 hook了系統的ClassLoader。而DroidPlugin則幾乎是完全自己實現了一套Framework層中對Service和Activity加載的方案,替換掉了系統幾乎全部相關的Binder
。 這樣的話Replugin的系統侵入性更低,維護成本更低,問題更少更加穩定。
Replugin爲了實現插件的管理會開啓一個常駐進程作爲service 端,其他插件進程都是client 端,而service端管理着所有插件的 安裝,卸載,更新,狀態等等功能,當插件進程client 需要實現(安裝,卸載,更新,狀態等等)這些功能的時候是通過進程間通信Binder機制實現的。這樣的思想和Android 系統的 AMS有點類似。
Replugin框架的註冊
Replugin中有一個RepluginApplication類直接繼承這個類就可以實現框架的註冊,也可以在自己Application類中回調的方法中調用相關方法,首先要調用的就是attachBaseContext()方法,要在這個方法中完成框架的初始化。
//com.qihoo360.replugin.RePlugin
public static void attachBaseContext(Application app, RePluginConfig config) {
//初始化一次
if (sAttached) {
return;
}
//這個類主要緩存application對象
RePluginInternal.init(app);
sConfig = config;
//初始化默認值
sConfig.initDefaults(app);
IPC.init(app);
// 初始化HostConfigHelper(通過反射HostConfig來實現)
// NOTE 一定要在IPC類初始化之後才使用
HostConfigHelper.init();
// FIXME 此處需要優化掉
AppVar.sAppContext = app;
//PluginStatusController用來管理插件的運行狀態:
//用來管理插件的狀態:正常運行、被禁用,等情況
//設置Application的引用
PluginStatusController.setAppContext(app);
PMF.init(app);
PMF.callAttach();
sAttached = true;
}
一 . 第一個要要講一下的是 sConfig.initDefaults(app);
//com.qihoo360.replugin.RePluginConfig
void initDefaults(Context context) {
if (pnInstallDir == null) {
pnInstallDir = context.getFilesDir();
}
if (callbacks == null) {
callbacks = new RePluginCallbacks(context);
}
if (eventCallbacks == null) {
eventCallbacks = new RePluginEventCallbacks(context);
}
}
1.獲取pn插件安裝目錄
2.創建RePluginCallbacks
3.創建RePluginEventCallbacks
首先RePluginCallbacks 這個類很重要主要用來生成宿主和插件的ClassLoader,是插件化的關鍵,這個類中還有相關的回調。下圖是主要的幾個方法。
RePluginEventCallbacks:插件化框架對外事件回調接口集,宿主需繼承此類,並複寫相應的方法來自定義插件框架的事件處理機制,其中有安裝插件成功、失敗、啓動插件Activity時的一些事件回調等。我們可以重寫這些方法然後再相應的回調中做相應的操作,比如插件安裝失敗彈出提示框等。
二,接下來是IPC.init(app);
//com.qihoo360.replugin.base.IPC;
public static void init(Context context) {
//獲取當前進程名稱
sCurrentProcess = SysUtils.getCurrentProcessName();
//獲取當前進程ID
sCurrentPid = Process.myPid();
//獲取當前包名
sPackageName = context.getApplicationInfo().packageName;
// 設置最終的常駐進程名
//------------------------------------------------------------
// RePlugin 坑位默認配置項
// 是否使用“常駐進程”(見PERSISTENT_NAME)作爲插件的管理進程
// public static boolean PERSISTENT_ENABLE = true;
// 常駐進程名
// public static String PERSISTENT_NAME = ":GuardService";
//------------------------------------------------------------
//如果設置常駐進程
if (HostConfigHelper.PERSISTENT_ENABLE) {
//cppn : :GuardService
String cppn = HostConfigHelper.PERSISTENT_NAME;
if (!TextUtils.isEmpty(cppn)) {
if (cppn.startsWith(":")) {
//常駐進程名稱爲 包名:GuardService
sPersistentProcessName = sPackageName + cppn;
} else {
sPersistentProcessName = cppn;
}
}
} else {
//如果不使用常駐進程管理插件,則使用當前app進程名稱
sPersistentProcessName = sPackageName;
}
//判斷當前進程是否是主進程
sIsUIProcess = sCurrentProcess.equals(sPackageName);
//判斷當前線程是不是常駐進程
sIsPersistentProcess = sCurrentProcess.equals(sPersistentProcessName);
}
IPC 的初始化主要是 獲取當前進程的名稱 ID 宿主的包名,然後設置“插件管理進程”的名稱。這裏框架默認是開啓一個新的進程“常駐進程”作爲“插件管理進程”PERSISTENT_ENABLE 的值默認是true。但是這個按鈕是可以關閉的。關閉後不會新開一個進程而是以主進程作爲插件管理進程。這樣就只有一個進程。Replugin 這樣靈活的設計都是爲了應用考慮。不同的APP需求不一樣,我們可以根據APP自己靈活使用,如需切換到以“主進程”作爲“插件管理進程”,則需要在宿主的app/build.gradle中添加下列內容,用以設置 persistentEnable 字段爲False
apply plugin: 'replugin-host-gradle'
repluginHostConfig {
// ... 其它RePlugin參數
// 設置爲“不需要常駐進程”
persistentEnable = false
}
RePlugin默認的“常駐進程”名爲“:GuardService”,通常在後臺運行,存活時間相對較久。各進程啓動時,插件信息的獲取速度會更快(因直接通過Binder從常駐進程獲取),只要常駐進程不死,其它進程殺掉重啓後,仍能快速啓動(熱啓動,而非“冷啓動”)但是若應用爲“冷啓動”(無任何進程時啓動),則需要同時拉起“常駐進程”,時間可能有所延長。若應用對“進程”數量比較敏感,則此模式會無形中“多一個進程” 。主進程也可以作爲“插件管理進程”無需額外啓動任何進程,例如你的應用只有一個進程的話,那採用此模型後,也只有一個進程,應用冷啓動(無任何進程時啓動)的時間會短一些,因爲無需再拉起額外進程。但是“冷啓動”的頻率會更高,更容易被系統回收,再次啓動的速度略慢於“熱啓動”。
三, PMF.init(app);
通過上面的介紹我們已經知道了Replugin有插件管理進程這一設計,接下來我們可以看看是如何實現這一設計思想的。也是整個插件最重要的地方PMF.init(app);
// com.qihoo360.loader2.PMF;
public static final void init(Application application) {
setApplicationContext(application);
PluginManager.init(application); // 1
sPluginMgr = new PmBase(application); //2
sPluginMgr.init();
Factory.sPluginManager = PMF.getLocal();
Factory2.sPLProxy = PMF.getInternal();
PatchClassLoaderUtils.patch(application);
}
首先 “1” 部分PluginManager.init(application); 初始化比較簡單創建了一個叫Tasks的類,在裏面中創建了一個主線程的Hanlder。方便後面執行任務,不必擔心Handler爲空的情況。
“2 ”部分 創建了Pmbase
// com.qihoo360.loader2.PmBase;
PmBase(Context context) {
mContext = context;
//判斷當前進程是UI進程(主進程)或者插件進程
if (PluginManager.sPluginProcessIndex == IPluginManager.PROCESS_UI || PluginManager.isPluginProcess()) {
String suffix;
//如果是主進程 設置suffix = N1;
if (PluginManager.sPluginProcessIndex == IPluginManager.PROCESS_UI) {
suffix = "N1";
} else {
//設值爲 是0或1
suffix = "" + PluginManager.sPluginProcessIndex;
}
//CONTAINER_PROVIDER_PART = .loader.p.Provider
mContainerProviders.add(IPC.getPackageName() + CONTAINER_PROVIDER_PART + suffix);
//CONTAINER_SERVICE_PART = ".loader.s.Service"
mContainerServices.add(IPC.getPackageName() + CONTAINER_SERVICE_PART + suffix);
}
mClient = new PluginProcessPer(context, this, PluginManager.sPluginProcessIndex, mContainerActivities);// "1"
mLocal = new PluginCommImpl(context, this);// "2"
mInternal = new PluginLibraryInternalProxy(this);// "3"
}
在PmBase 的構造函數中首先UI進程帶有N1標識,其他標識爲 0 或 1 。然後拼接service 和 provider 名稱 將其存在hashset 中。不同的進程拼接的名稱是不一樣的。主要就是之前這個標識起到區分作用,然後這個service 和 provider 名字會在編譯的時候通過gradle自動添加到我們的AndroidManifest.xml文件中完成註冊。
“1” 創建了PluginProcessPer 對象,這個類 extends IPluginClient.Stub 。這是熟悉的AIDL的味道,那麼我們知道這個PluginProcessPer是個Binder對象,我們通過這個類來和服務端進行通信。在這個類的構造函數中我們看到初始化了PluginServiceServer,這個類主要負責Server端的服務調度、提供等工作,是服務的提供方,核心類之一。另一個是PluginContainers,用來管理Activity坑位信息的容器,初始化了多種不同啓動模式和樣式Activity的坑位信息。
“2” 創建PluginCommImpl對象。這個類主要負責宿主與插件、插件間的互通,可通過插件的Factory直接調用,也可通過RePlugin來跳轉
“3” 創建PluginLibraryInternalProxy對象。通過“反射”調用的內部邏輯(如PluginActivity類的調用、Factory2等)
這 “1” “2” “3”的內容涉具體的會在之後講。
在構建好PmBase對象後執行其init();
// com.qihoo360.loader2.PmBase;
void init() {
RePlugin.getConfig().getCallbacks().initPnPluginOverride();
if (HostConfigHelper.PERSISTENT_ENABLE) {
// (默認)“常駐進程”作爲插件管理進程,則常駐進程作爲Server,其餘進程作爲Client
if (IPC.isPersistentProcess()) {
//如果當前進程是插件管理進程
// 初始化“Server”所做工作
initForServer();// 1
} else {
// 如果當前進程是client 初始化client
initForClient(); //2
}
} else {
// “UI進程”作爲插件管理進程(唯一進程),則UI進程既可以作爲Server也可以作爲Client
if (IPC.isUIProcess()) {
// 1. 嘗試初始化Server所做工作,
initForServer();
// 2. 註冊該進程信息到“插件管理進程”中
// 注意:這裏無需再做 initForClient,因爲不需要再走一次Binder
PMF.sPluginMgr.attach();
} else {
// 其它進程?初始化Client
initForClient();
}
}
// 從mPlugins中將所有插件信息取出,保存到PLUGINS中,PLUGINS是一個HashMap,保存的key是包名或者別名,value是PluginInfo
PluginTable.initPlugins(mPlugins);
}
這裏首先判斷是否啓用了“常駐進程”作爲插件管理進程,如果啓用了就判斷是否是插件管理進程,之前我們介紹過,插件管理進程是服務端,其他進程都是客戶端。會根據不同的進程做不同的初始化,如果沒用啓用“常駐進程”那麼UI 進程就是服務端,那麼UI進程既可以是server 也可以是client 。這個時候不需要再initForClient() 因爲兩個邏輯在同一個進程中不需要再一次Binder進行進程間通信。最後把所有的插件信息取出來保存到HashMap中。
"1" initForServer,服務端的初始化
// com.qihoo360.loader2.PmBase;
private final void initForServer() {
// 1 創建PmHostSvc
mHostSvc = new PmHostSvc(mContext, this);
// 2 創建進程管理類 PluginProcessMain
PluginProcessMain.installHost(mHostSvc);
StubProcessManager.schedulePluginProcessLoop(StubProcessManager.CHECK_STAGE1_DELAY);
mAll = new Builder.PxAll();
Builder.builder(mContext, mAll);
//將剛掃描的本地插件封裝成Plugin添加進mPlugins中,mPlugins代表所有插件的集合
refreshPluginMap(mAll.getPlugins());
try {
// 調用常駐進程的Server端去加載插件列表,方便之後使用
List<PluginInfo> l = PluginManagerProxy.load();
if (l != null) {
// 將"純APK"插件信息併入總的插件信息表中,方便查詢
// 這裏有可能會覆蓋之前在p-n中加入的信息。本來我們就想這麼幹,以"純APK"插件爲準
refreshPluginMap(l);
}
} catch (RemoteException e) {
}
}
“1”創建PmHostSvc對象。extends IPluginHost.Stub 又是一個binder 對象,在他的構造函數中
// com.qihoo360.loader2.PmHostSvc;
PmHostSvc(Context context, PmBase packm) {
mContext = context;
mPluginMgr = packm;
//創建一個service管理者
mServiceMgr = new PluginServiceServer(context);
//創建一個插件管理者
mManager = new PluginManagerServer(context);
}
“2”創建進程管理類PluginProcessMain 插件管理進程調用,緩存自己的 IPluginHost
// com.qihoo360.loader2.PluginProcessMain;
static final void installHost(IPluginHost host) {
sPluginHostLocal = host;
// 連接到插件化管理器的服務端
try {
PluginManagerProxy.connectToServer(sPluginHostLocal);
} catch (RemoteException e) {
// 基本不太可能到這裏,直接打出日誌
if (LOGR) {
e.printStackTrace();
}
}
}
//com.qihoo360.replugin.packages.PluginManagerProxy
/**
* 連接到常駐進程,並緩存IPluginManagerServer對象
*
* @param host IPluginHost對象
* @throws RemoteException 和常駐進程通訊出現異常
*/
public static void connectToServer(IPluginHost host) throws RemoteException {
if (sRemote != null) {
return;
}
//host 是IPluginHost 真正調用fetchManagerServer方法的是我們繼承IPluginHost.Stub的Binder對象PmHostSvc
sRemote = host.fetchManagerServer();
}
// com.qihoo360.loader2.PmHostSvc;
//PmHostSvc 類中的fetchManagerServer()方法
@Override
public IPluginManagerServer fetchManagerServer() throws RemoteException {
//mManager 是在PmHostSvc構造方法中初始化的PluginManagerServer
return mManager.getService();
}
這裏PluginManagetServer.getService()得到的是 private class Stub extends IPluginManagerServer.Stub 這個Stub Binder對象。又是AIDL 那麼我們找找aidl文件看看服務端提供了哪些方法。
//com.qihoo360.replugin.packages.IPluginManagerServer.aidl
interface IPluginManagerServer {
/**
* 安裝一個插件
*/
PluginInfo install(String path);
/**
* 卸載一個插件
*/
boolean uninstall(in PluginInfo info);
/**
* 加載插件列表,方便之後使用
*/
List<PluginInfo> load();
/**
* 更新所有插件列表
*/
List<PluginInfo> updateAll();
/**
* 設置isUsed狀態,並通知所有進程更新
*/
void updateUsed(String pluginName, boolean used);
......
}
首先服務端也就是插件管理進程創建了一個Binder對象PmHostSvc 然後PmHostSvc創建PluginManagerServer 插件管理者通過內部的Binder對象Stub(實現IPluginManagerServer接口)提供了插件安裝,卸載,更新等操作。因此我們的插件管理進程創建了進程管理類PluginProcessMain緩存了IPluginManagerServer對象(也就是Stub)用來提供插件安裝,卸載,更新等服務。在緩存完畢後管理進程開始掃描本地的插件,然後同步所有的插件信息,最後判斷是否有插件需要更新或刪除,對應的執行一些操作。服務端也就是插件管理進程的初始化就完成了。
另外PmHostSvc 還創建了一個service 管理者PluginServiceServer
然後initForClient ,客戶端的初始化
//com.qihoo360.loader2.PmBase
/**
* Client(UI進程)的初始化
*
*/
private final void initForClient() {
// 1. 先嚐試連接
PluginProcessMain.connectToHostSvc();
// 2. 然後從常駐進程獲取插件列表
refreshPluginsFromHostSvc();
}
/**
* 非常駐進程調用,獲取常駐進程的 IPluginHost
*/
static final void connectToHostSvc() {
Context context = PMF.getApplicationContext();
// 1
IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);
。。。。
//通過調用asInterface方法確定是否需要返回遠程代理
sPluginHostRemote = IPluginHost.Stub.asInterface(binder);
// 連接到插件化管理器的服務端
try {
PluginManagerProxy.connectToServer(sPluginHostRemote);
// 將當前進程的"正在運行"列表和常駐做同步
// TODO 若常駐進程重啓,則應在啓動時發送廣播,各存活着的進程調用該方法來同步
PluginManagerProxy.syncRunningPlugins();
} catch (RemoteException e) {
// 獲取PluginManagerServer時出現問題,可能常駐進程突然掛掉等,當前進程自殺
System.exit(1);
}
// 註冊該進程信息到“插件管理進程”中
PMF.sPluginMgr.attach();
}
"1" PluginProviderStub.proxyFetchHostBinder(context);
//com.qihoo360.loader2.PluginProviderStub
private static final IBinder proxyFetchHostBinder(Context context, String selection) {
//
Cursor cursor = null;
try {
//uri 經過拼接後: content://包名.loader.p.main/main
Uri uri = ProcessPitProviderPersist.URI;
cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
if (cursor == null) {
return null;
}
...
//通過cursor得到Binder對象
IBinder binder = BinderCursor.getBinder(cursor);
return binder;
} finally {
CloseableUtils.closeQuietly(cursor);
}
}
這裏我們看到IBinder 對象是通過getBinder(cursor)獲取到的,而cursor 是通過provider query獲得的,進入代碼我們看到cursor是BinderCursor.queryBinder(PMF.sPluginMgr.getHostBinder()),裏面傳入的參數是PMF.sPluginMgr.getHostBinder(),sPluginMgr是最開始初始化的PmBase 。而getHostBinder 返回的對象就是之前在initForServer()中創建的mHostSvc = new PmHostSvc(mContext, this);所以從cursor中取出的Binder就是PmHostSvc。
另外我們在使用AIDL的時候回創建一個Service 客戶端通過bindService 來獲取IBinder, 但是通過bindServive 是一個異步的過程。我們必須等到ServiceConnection
拿到 onServiceConnected
回調。這個過程是異步的,這樣的話,必須等待才能執行之後的連接服務端,和插件管理進程進行信息同步。爲了規避這個問題。採用ProviderContent 直接得到PmHostSvc就更加合適。
得到PmHostSvc通過調用asInterface方法確定是否需要返回遠程代理。然後連接服務端,把Client的信息和插件管理進程進行信息同步。然後把client進程註冊到插件管理進程中。initForClient()工作就完成了。在上面各進程操作完後會遍歷mPlugins中的所有插件信息,並將他們存入到一個叫PluginTable類中一個叫PLUGINS的HashMap中,存入的key是包名或者別名,value是PluginInfo。
到這裏我們講完了PMF.init(app) ,中 PmBase.init(); 在PMF.init(app) 中還有兩行代碼,我們新開一章繼續看。