Android應用程式外掛化研究之Activity註冊
最近在研究Android應用的外掛化開發,看了好幾個相關的開源專案。外掛化都是在解決以下幾個問題:
- 如何把外掛apk中的程式碼和資源載入到當前虛擬機器。
- 如何把外掛apk中的四大元件註冊到程序中。
- 如何防止外掛apk中的資源和宿主apk中的資源引用衝突。
在上篇文章中我研究了如何獲取並使用外掛apk中的資源的問題(文字、圖片、佈局等),前面兩篇文章解決了外掛化研究的第一個問題。本篇文章開始研究第二個問題:“註冊”外掛中的四大元件。
在安裝apk的時候,應用管理服務PackageManagerService會解析apk,解析應用程式配置檔案AndroidManifest.xml,並從裡面得到得到應用得到應用程式的元件Activity、Service、Broadcast Receiver和Content Provider等資訊,對應用的每個元件“登記”,“登記”之後,在啟動某個Activity過程在ASM執行時對比“登記”然後“查有此人”允許後續的啟動行為。詳細過程可以參考
一、 代理方式實現。
宿主端實現一個 PluginProxyActivity,使用這個Activity代理外掛中的Activity的重要事務,例如生命週期呼叫、contentview設定、Activity跳轉等事務。PluginProxyActivity註冊在宿主中,啟動外掛中的Activity實際就是啟動PluginProxyActivity,只是載入的佈局和方法邏輯不一樣而已。百度的外掛框架
二、“佔坑”方式實現。
啟動Activity是一個複雜的過程,有很多環節:Activity.startActivity()->Activity.startActivityForResult()->Instrument.excuteStartActivity()->ASM.startActivity()。大概又這麼幾個環節,詳細瞭解可以參考文章:《深入理解Activity的啟動過程》。 所謂“佔坑”在宿主端的AndroidManifest.xml註冊一個不存在的Activity,可以取名為StubActivity,同樣啟動外掛的Activity都是啟動StubActivity,然後在啟動Activity的某個環節,我們找個“臨時”演員來代替StubActivity,這個臨時演員就是外掛中定義的Activity,這叫“瞞天過海”。如何找“臨時”演員,這個過程又有很多種實現手段,DroidPlugin、dwarf等框架實現手段各有不同,詳細後續文章再討論。
簡單解釋了兩種思路,本文先用demo來說說如何實現第一種思路(後續文章研究下第二思路)。
PluginProxyActivity的實現
一、重寫setContentView(int layoutResID)方法,使用外掛的AssertManager載入佈局資源。該方法提供給外掛Activity呼叫。
@Override
public void setContentView(int layoutResID) {
// do something plugin need
Resources resources = PluginManager.getInstace().getResources();
XmlPullParser xmlResourceParser = resources.getLayout(layoutResID);
View viewFromPlugin = LayoutInflater.from(this).inflate(xmlResourceParser, null);
setContentView(viewFromPlugin);
}
二、 重寫onCreate(Bundle savedInstanceState)方法,在這個方法中通過外掛的Activity的類名,利用反射例項化外掛Activity物件,並呼叫其onCreate方法。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String className = getIntent().getStringExtra("class");
initPluginInstance(className);
invokePluginOnCreate(savedInstanceState);
}
private void initPluginInstance(String className) {
try {
pluginClass = PluginManager.getInstace().getCloassLoader().loadClass(className);
Constructor<?> localConstructor = pluginClass.getConstructor(new Class[]{});
pluginInstance = localConstructor.newInstance(new Object[] {});
// 把當前的代理Activity注入到外掛中
Method setProxy = pluginClass.getMethod("setProxy",
new Class[]{PluginProxyActivity.class});
setProxy.setAccessible(true);
setProxy.invoke(pluginInstance, new Object[] { this });
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, e.getMessage());
}
}
private void invokePluginOnCreate(Bundle savedInstanceState) {
try {
Method onCreate = pluginClass.getDeclaredMethod("onCreate",
new Class[]{Bundle.class});
onCreate.setAccessible(true);
onCreate.invoke(pluginInstance, new Object[] { savedInstanceState });
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
三、 重寫其他生命週期函式並利用反射呼叫外掛Activity的對應的生命週期函式,例如onPause方法。
@Override
protected void onPause() {
super.onPause();
invokePluginOnPause();
}
private void invokePluginOnPause() {
try {
Method onPause = pluginClass.getDeclaredMethod("onPause",
new Class[]{});
onPause.setAccessible(true);
onPause.invoke(pluginInstance, new Object[] {});
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
四、 過載startAcivity(String className)方法,也就是使用使用定製的startActivity方法來啟動外掛Activity啦。
public void startActivity(String className) {
// do something plugin need
Intent intent = new Intent(this,PluginProxyActivity.class);
intent.putExtra("class",className);
startActivity(intent);
}
約定外掛Activity,外掛Activity基類:BasePluginActivity的實現
一、 提供public void setProxy(PluginProxyActivity proxyPluginAct)方法,以獲得PluginProxyActivity的引用。
/**
*
* @param proxyPluginAct
* provided this method to invoke by reflect . inject proxy
*/
public void setProxy(PluginProxyActivity proxyPluginAct){
mProxy = proxyPluginAct;
}
二、重寫setContentView(int layoutResID),呼叫proxy的setContentView(int layoutResID)方法。
/**
* set layout to proxyActivity
* @param layoutResID
*/
@Override
public void setContentView(int layoutResID) {
if (mProxy != null){
mProxy.setContentView(layoutResID);
} else {
super.setContentView(layoutResID);
}
}
三、 定製startActivity(String className)方法來啟動Activity,呼叫proxy的startActivity方法。
public void startActivity(String className) {
mProxy.startActivity(className);
}
最重要的demo
demo實現啦一個外掛的框架的最基本雛形,地址:https://github.com/liuguangli/ProxyPluginActivityDemo,如果你有興趣,一定要star,日後研究研究。代理方式實現起來比較簡單,也比較好理解,但是有很多缺陷:一、外掛Activity不能使用this關鍵字,比如this.finish()方法是無效的,真正掌管生命週期的是proxy應該呼叫proxy.finish(),所以百度開源框架
dynamic-load-apk使用that指向proxy,約定外掛中使用that來代替this。二、 外掛Activity無法深度演繹真正的Activity元件,可能有些高階特性無法使用。
總之,不夠透明,外掛開發需要定義自己的規範。既然如此,有沒有更好的方案?當然有,後續文章繼續研究外掛化如何註冊元件的第二類思路:“佔坑”方式實現外掛Activity的註冊。