Android外掛化原理和實踐 (六) 之 四大元件解決方案
在前面的幾篇文章中已經介紹完了Android外掛化的第一和第二個根本問題,就是宿主和外掛的程式碼互相呼叫問題和外掛中資源的讀取問題。現剩下的就是Android外掛化裡最麻煩的第三個根本問題,也就是在外掛中使用四大元件的問題。我們知道,目前外掛中的四大元件要想正常使用就必須要在宿主中的AndroidManifest.xml中提前宣告好,因為四大元件在啟動過程中只認宿主中的AndroidManifest.xml,外掛中可以不需要宣告。那樣的話,在開發過程中難免會非常不方便,要想在外掛中增加一個Activity或Service那就必須同時修改宿主,這樣就違背了外掛化免安裝和靈活更新的初衷。
我們在前面第一篇文章中也提到了什麼情況需要外掛化問題上,有一點是在模組功能相對成熟穩定後才比較適合去做成外掛化。因為到了模組工程的後期更新幅度不會太大,我們只需要在宿主AndroidManifest.xml中預留宣告幾個命名不太友好的Activity和Service的空宣告就能應付一般情況下的更新,等到下次大版本連宿主一起更新時再來重構回來或又再新增幾個坑位以備不時之需。
1 Activity 的解決方案
其實目前大多數外掛化框架的最大差異地方就是在於如何在解決外掛中使用四大元件上做得更好,針對此情況大致有兩大流派:
動態替換方案,以Activity為例,主要是對Android底層程式碼進行Hook,使在App啟動Activity中進行欺騙ActivityManagerService,以達到載入外掛中元件的目的。
靜態代理方案,以Activity為例,通過宿主中的一個代理Activity統一操縱外掛中的所有Activity。
1.1 動態替換方案
動態替換方案做得最好的應該是360的DroidPlugin外掛化框架,它可以做到不需在宿主中去宣告外掛中的元件資訊,意義上已經可以載入任何別人家的apk了。它的思路大概是:不合並外掛中的dex,因為LoadedApk是儲存apk四大元件資訊和其它資訊的型別,框架程式碼中為外掛也建立了一個LoadedApk物件,並放於ActivityThread的mPackages陣列變數中,再為每個外掛都建立一個ClassLoader,然後把LoadedApk物件的ClassLoader改為外掛的ClassLoader。這樣等一系列的反射中就能達到啟動外掛Activity的目的,但是缺點也是相當明顯,就是要反射的地方非常多,而且還要適配不同的Android版本。我們並不打算去分析這個複雜的過程,知道是大概原理就好了。
有一種相對比較簡單一點的方案,就是欺騙ActivityManagerService,目前像ACCD外掛化框架就是使用這方案。思路大概是先在宿主中定義一個替身Activity,然後也是在啟動Activity過程中通過反射Hook兩處地方程式碼,第一處是在準備跨程序呼叫ActivityManagerSerfvice前,即Instrumentation.java的execStartActivity方法中通過ActivityManagerNative.getDefault()它返回一個IActivityManager物件,我們對它建立一個代理,在IActivityManager物件去呼叫startActivity前把目標Activity替換成替身Activity,以達到欺騙目的。然後第二處
public class AMSHookHelper {
public static final String EXTRA_TARGET_INTENT = "extra_target_intent";
public static void hookActivity() {
try {
hookAMN();
hookActivityThread();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 第一處Hook地方,Hook ActivityManagerNative中通過getDefault方法獲得的物件,使其在呼叫startActivity時替換成替身Activity,以達到欺騙ActivityManagerSerfvice的目的
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private static void hookAMN() throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
// 獲取 ActivityManagerNative 的 gDefault
Class activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
Object gDefaultObj = gDefaultField.get(null);
// 獲取 gDefault 對應在 android.util.Singleton<T> 的單例物件 mInstance
Class singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
final Object mInstanceObj = mInstanceField.get(gDefaultObj);
// 建立 gDefault 的代理
Class<?> classInterface = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] { classInterface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只 Hook startActivity 一個方法
if (!"startActivity".equals(method.getName())) {
return method.invoke(mInstanceObj, args);
}
// 找到引數裡面的Intent 物件
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
Intent targetIntent = (Intent) args[index];
boolean isExistActivity = isExistActivity(targetIntent.getComponent().getClassName());
if (isExistActivity) {
return method.invoke(mInstanceObj, args);
}
String stubPackage = targetIntent.getComponent().getPackageName();
Intent newIntent = new Intent();
newIntent.setComponent(new ComponentName(stubPackage, StubActivity.class.getName()));
// 把原來要啟動的目標Activity先存起來
newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, targetIntent);
// 替換掉Intent, 即欺騙AMS要啟動的是替身Activity
args[index] = newIntent;
return method.invoke(mInstanceObj, args);
}
}
);
//把 gDefault 的 mInstance 欄位,替換成 proxy
mInstanceField.set(gDefaultObj, proxy);
}
/**
* 判斷Activity是否有在宿主中宣告
* @param activity
* @return
*/
private static boolean isExistActivity(String activity) {
try {
PackageManager packageManager = HostApplication.getContext().getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(HostApplication.getContext().getPackageName(), PackageManager.GET_ACTIVITIES);
ActivityInfo[] activities = packageInfo.activities;
for(int i = 0; i < activities.length; i++) {
if (activity.equalsIgnoreCase(activities[i].name)) {
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 第二處Hook地方,Hook ActivityThread 中的 Handle mCallback 物件,使接收 LAUNCH_ACTIVITY 訊息後將替身 Activity 換回目標 Activity
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private static void hookActivityThread() throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
// 獲取到當前的 ActivityThread 物件
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Object currentActivityThreadObj = sCurrentActivityThreadField.get(null);
// 獲取 ActivityThread 物件中的 mH 變數
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mHObj = (Handler) mHField.get(currentActivityThreadObj);
// Hook Handler 的 mCallback 欄位
Class handlerClass = Handler.class;
Field mCallbackField = handlerClass.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mHObj, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
int LAUNCH_ACTIVITY = 0;
try {
Class hClass = Class.forName("android.app.ActivityThread$H");
Field launchActivityField = hClass.getDeclaredField("LAUNCH_ACTIVITY");
launchActivityField.setAccessible(true);
LAUNCH_ACTIVITY = (int) launchActivityField.get(null);
if (msg.what == LAUNCH_ACTIVITY) {
// 把替身 Activity 的 Intent 恢復成目標 Activity 的 Intent
Class c = msg.obj.getClass();
Field intentField = c.getDeclaredField("intent");
intentField.setAccessible(true);
Intent intent = (Intent) intentField.get(msg.obj);
Intent targetIntent = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
intent.setComponent(targetIntent.getComponent());
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 走完原來流程
mHObj.handleMessage(msg);
return true;
}
});
}
}
上述方法只要在startActivity之前找個時機呼叫一下AMSHookHelper.hookActivity();即可,這裡還需要在宿主中建立一個替身Activity:
public class StubActivity extends Activity {
}
這個StubActivity啥都不用做,就這樣就能解決不在宿主中宣告外掛的Activity也能正常啟動。這個方案雖然也比較簡單,但畢竟還是反射了系統底層程式碼,所以在往後Android版本更新時還要留意是否有相容問題。還要注意的是,此方案只對LaunchMode是standard才能生效,如果是SingleTop、SingleTask和SingleInstance的話,一般就要加上使用佔位思想,就是提前在宿主中各定義N個這3類的Acitivity,然後通過對應關係將外掛中的Activtity和這些佔位Activity繫結起來,做一個順序迴圈使用。這裡就不作詳細介紹。
1.2 靜態代理方案
靜態代理方案相對來說,會比較好理解一點。因為它不需要去Hook任何程式碼,主要在宿主中建立一個代理的Activity,叫ProxyActivity,讓這個ProxyActivity內部有一個對外掛Activity的引用,讓ProxyActivity的任何生命週期函式都呼叫外掛中的Activity中同名的函式。這也是dynamic-load-apk外掛化框架所創,這方案最有趣的一點就是存在一個that關鍵字。來通過程式碼看看它的實現。
第一步,也是最關鍵一步,在宿主中建立一個ProxyActivity,並新增如下程式碼:
public class ProxyActivity extends Activity {
private Object mRemoteActivity;
private String mClass;
private HashMap<String, Method> mActivityLifecircleMethods = new HashMap<String, Method>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mClass = getIntent().getStringExtra(Coordinator.EXTRA_CLASS);
launchTargetActivity();
invokeOnCreate();
}
/**
* 反射目標 Activity
*/
void launchTargetActivity() {
try {
// 獲取外掛的 Activity 物件
Class<?> localClass = Class.forName(mClass);
Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
mRemoteActivity = localConstructor.newInstance(new Object[] {});
// 執行外掛 Activity 的 setProxy 方法,傳遞this過去,使建立雙向引用
Method setProxy = localClass.getMethod("setProxy", new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(mRemoteActivity, new Object[] { this });
// 反射外掛 Activity 的生命週期函式
launchTargetActivityLifecircleMethods(localClass);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 反射外掛 Activity 的生命週期函式
* @param localClass
*/
protected void launchTargetActivityLifecircleMethods(Class<?> localClass) {
Method onCreate = null;
try {
onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });
onCreate.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put("onCreate", onCreate);
String[] methodNames = new String[]{"onRestart", "onStart", "onResume", "onPause", "onStop", "onDestory"};
for (String methodName : methodNames) {
Method method = null;
try {
method = localClass.getDeclaredMethod(methodName, new Class[]{});
method.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put(methodName, method);
}
Method onActivityResult = null;
try {
onActivityResult = localClass.getDeclaredMethod("onActivityResult",
new Class[] { int.class, int.class, Intent.class });
onActivityResult.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put("onActivityResult", onActivityResult);
}
/**
* 執行外掛 Activity 的 onCreate 方法
*/
private void invokeOnCreate() {
Method onCreate = mActivityLifecircleMethods.get("onCreate");
if (onCreate != null) {
try {
Bundle bundle = new Bundle();
onCreate.invoke(mRemoteActivity, new Object[]{bundle});
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
invokeOnActivityResult(requestCode, resultCode, data);
}
private void invokeOnActivityResult(int requestCode, int resultCode, Intent data) {
Method onActivityResult = mActivityLifecircleMethods.get("onActivityResult");
if (onActivityResult != null) {
try {
onActivityResult.invoke(mRemoteActivity, new Object[] { requestCode, resultCode, data });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onStart() {
super.onStart();
invokeOnStart();
}
private void invokeOnStart() {
Method onStart = mActivityLifecircleMethods.get("onStart");
if (onStart != null) {
try {
onStart.invoke(mRemoteActivity, new Object[] {});
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onRestart() {
super.onRestart();
invokeOnRestart();
}
private void invokeOnRestart() {
Method onRestart = mActivityLifecircleMethods.get("onRestart");
if (onRestart != null) {
try {
onRestart.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onResume() {
super.onResume();
invokeOnResume();
}
private void invokeOnResume() {
Method onResume = mActivityLifecircleMethods.get("onResume");
if (onResume != null) {
try {
onResume.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
super.onPause();
invokeOnPause();
}
private void invokeOnPause() {
Method onPause = mActivityLifecircleMethods.get("onPause");
if (onPause != null) {
try {
onPause.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onStop() {
super.onStop();
invokeOnStop();
}
private void invokeOnStop() {
Method onStop = mActivityLifecircleMethods.get("onStop");
if (onStop != null) {
try {
onStop.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
invokeOnDestroy();
}
private void invokeOnDestroy() {
Method onDestroy = mActivityLifecircleMethods.get("onDestroy");
if (onDestroy != null) {
try {
onDestroy.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ProxyActivity在onCreate方法中,接收外掛目標Activity的名稱,並反射它的例項和生命週期方法,其間將自己this傳遞給外掛目標Activity,形成雙向引用,最後呼叫了外掛目標Activity的onCreate方法。
第二步,在外掛中建立一個外表很像Activity的類BasePluginActivity:
public class BasePluginActivity {
protected Activity that;
public void setProxy(Activity proxyActivity) {
that = proxyActivity;
}
protected void onCreate(Bundle savedInstanceState) {
}
public void setContentView(int layoutResID) {
that.setContentView(layoutResID);
}
}
此類就是用於外掛中所有Activity的基類,也就是它接收了宿主ProxyActivity傳遞過來的引用,這裡用變數that來儲存。
第三步,在外掛中建立目標Activity,並使其繼承BasePluginActivity:
public class PluginActivity extends BasePluginActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin2);
}
}
這是一個看起來正常不過的Activity,但是其實它的onCreate和setContentView方法都是呼叫了它的父類BasePluginActivity同名方法,然後通過父類中的that指向宿主中ProxyActivity的同名方法。
第四步,就是呼叫外掛中的Activity了:
Intent intent = new Intent();
intent.setClass(MainActivity.this, ProxyActivity.class);
intent.putExtra(Coordinator.EXTRA_CLASS, "com.zyx.plugin.PluginActivity");
startActivity(intent);
所有要啟動外掛中的Activity時,都必要啟動宿主的ProxyActivity,然後將外掛Activity的完整類名附加到EXTRA_CLASS引數中。
就這樣,靜態代理方案也就完成了。其實此方案所做的一系列事情還是比較好理解,不像動態替換方案那樣,還需先把Activity的啟動過程先理解一遍,而且也不用擔心會因為Android的版本更新而導致反射失敗。但是同樣要注意的是,此方案也只對LaunchMode是standard才能生效。
2 Service的解決方案
對於同一個Service呼叫多次startService並不會啟動多個Service例項,所以如果按照Activity的動態替換方案,弄一個替身StubService是應付不了多外掛Service的。其實在我們日常發開中,絕大部分情況Service的數量不超過10個,Service也不像Activity那樣有不同的LaunchMode。有些外掛化框架會預先建立10個StubService佔位,然後還是通過Hook的辦法,再加上一個配置表來繫結外掛中哪個Service和宿主中幾號StubService進行關係繫結來處理。這辦法也是耗盡心思做了差強人意的事情,以其那樣,還不如就直接在宿主中的AndroidManifest.xml中只預先宣告10個備用的Service,然後在外掛中需要使用時,外掛也命名一個同名的Service來得痛快,雖然這樣的話外掛中的Service命名會很奇怪,但是我們可以用註釋來標明Service的用途和對宿主中AndroidManifest.xml中標註哪些Service宣告已經被使用,這樣開發起來也簡單,而且不需要去維護那個繫結關係的配置表。
3 BroadcastReceiver的解決方案
BroadcastReceiver分動態註冊和靜態註冊兩種,對於動態註冊廣播,外掛化已經預設支援了,因為它不需要在AndroidManifest.xml中去註冊宣告,而是通過程式碼就能註冊。而對於靜態註冊廣播,也可以藉助在宿主AndroidManifest.xml中預宣告佔位來解決,而且一個外掛只佔一個位就足夠了,因為廣播都要攜帶一個或多個Action,我們在外掛中可以通過Action來區分做事情。
4 ContentProvider的解決方案
Contentprovider就是一個數據庫引擎,向外界提供增刪改查的介面。它在外掛化中的解決方案完全可以借鑑BroadcastReceiver方案,藉助在宿主AndroidManifest.xml中預宣告佔位,也是一個外掛只佔一個位。然後通過URI來區分做事情。
5 總結
外掛化中四大元件的解決從來都是開發者最頭痛的事情。上述解決方案中,你是不是覺得Service、BroadcastReceiver和ContentProvider的解決方案很粗糙?是又怎麼樣,誰叫它們使用的場景沒有Activity多。其實Activity除了LaunchMode是standard外的解決方案也並不完美。當然目前熱門的那幾個外掛化框架肯定沒有這麼粗糙的解決,筆者這樣只是做一個簡單的入門介紹和思路分析。還是要再次強調那句建議:專案中模組功能相對成熟穩定後才比較適合去做成外掛化!