Android外掛化Step 1 - Activity的生命週期管理
首先說明下該文是基於Android8.0,目前網上大多是外掛化機制方案部落格都比較舊,Android 8.0較之前的改了很多,所以之前方案原理依然使用,但是實現程式碼需要改動
Android外掛化技術是一項很實用的技術,一些大的廠商都在使用。該技術簡言之就是執行一個未安裝的apk。apk一般來說是肯定需要先下載到本地執行安裝後才能正常使用。但是一個apk如果功能很多,比如支付寶,那麼它的apk體積會很大。但是實際上它的apk也並沒有多大,主要就是因為它的每個小功能都當作一個獨立外掛,使用的時候才去動態載入。這就是Android外掛化技術。這個技術的優點有很多,比如縮小apk體積、外掛與宿主獨立開發等等。想具體瞭解外掛化技術的前身今世可以看下這個 ofollow,noindex">Android外掛化:從入門到放棄
DroidPlugin是360公司開發的一個十分成熟Android外掛化方案,這裡我們只簡要學習一下其中的原理,能更好的理解整個Android的Framework層,對以後無論是Android開發還是其他Android相關工作都有很大幫助(吧)。關於Android最重要的我認為有兩步:
Step 1 :Activity的生命週期管理
Step 2 :外掛的載入機制
Step 1 在於如何啟動一個未在AndroidManifest.xml註冊的Activity。Step 2 在於宿主如何去載入外掛的apk從而啟動其中的Activity。(這裡首先明確兩個概念宿主與外掛,宿主即是要啟動外掛的App,比如支付寶;外掛是那個未安裝的apk,比如支付寶裡面的小外掛-‘我的快遞’等等。)
本文主要講述基於 Android 8.0 系統如何啟動一個未註冊Activity的原理,(也就是說這個Activity在宿主的Apk裡,只是沒有註冊,並不是外掛的Activity,如何啟動外掛下一步會講)具體實現可以用java反射動態代理的方式實現,我是基於Xposed HOOK的方法做的,可能大家都不用這種方法,所以下面的程式碼部分我沒有貼我的,而是用的weishu這位大神的,下面會貼上他的連結。主要在原理解析。
AndroidManifest.xml的限制
相信大家在Android開發過程中都遇到過下面這個BUG,一旦大家在寫了一個Activity但是沒有在AndroidManifest.xml的註冊,那麼就會遇到這個問題。
E/AndroidRuntime﹕ FATAL EXCEPTION: main Process: com.xxx.xxx, PID: xxx android.content.ActivityNotFoundException: Unable to find explicit activity class {com.xxx.xxx.xxx.TargetActivity}; have you declared this activity in your AndroidManifest.xml?
啟動Activity確實非常簡單,但是Android卻有一個限制: 必須在AndroidManifest.xml中顯示宣告使用的Activity 。這個硬性要求很大程度上限制了外掛系統的發揮:假設我們需要啟動一個外掛的Activity,外掛使用的Activity是無法預知的,這樣肯定也不會在Manifest檔案中宣告;如果外掛新新增一個Activity,主程式的AndroidManifest.xml就需要更新;既然雙方都需要修改升級,何必要使用外掛呢?這已經違背了動態載入的初衷:不修改外掛框架而動態擴充套件功能。
但是我們可以耍個障眼法:既然AndroidManifest檔案中必須宣告,那麼我就宣告一個(或者有限個)替身Activity好了,當需要啟動外掛的某個Activity的時候,先讓系統以為啟動的是AndroidManifest中宣告的那個替身,暫時騙過系統;然後到合適的時候又替換回我們需要啟動的真正的Activity。所以首先要了解Activity的啟動過程。
Activity外掛化原理
我們開發的時候啟動一個Activity就調startActivity就可以了,那麼startyActivity這個呼叫背後發生了什麼?這就需要看原始碼一步一步看下去了。這裡可以百度看看其他人的部落格,自己對著部落格和原始碼一步一步看下去,就能完整的理解這個過程了。
大致啟動過程如下所示,應用程序通過startActivity啟動activity之後會通過ActivityManagerProxy向系統程序(ActivityManagerService所在程序)傳送Binder通訊,讓AMS啟動一個Activity,之後AMS會檢測Acticity是否註冊,然後再通過Binder請求讓應用啟動Activity。這個檢測過程發生在AMS所在的程序system_server。

Activity啟動過程
App程序會委託AMS程序完成Activity生命週期的管理以及任務棧的管理;這個通訊過程AMS是Server端,App程序通過持有AMS的client代理ActivityManagerNative完成通訊過程;
AMS程序完成生命週期管理以及任務棧管理後,會把控制權交給App程序,讓App程序完成Activity類物件的建立,以及生命週期回撥;這個通訊過程也是通過Binder完成的,App所在server端的Binder物件存在於ActivityThread的內部類ApplicationThread;AMS所在client通過持有IApplicationThread的代理物件完成對於App程序的通訊。
所以我們只要在AMP向AMS傳送請求之前替換我們的請求資訊,把我們要啟動的外掛Activity替換成我們提前宣告好的替身Activity就好,這樣AMS檢測就不會出問題。然後等AMS嚮應用程序返回訊息之後我們再把外掛Activity替換回來即可。如圖:

外掛化原理
外掛化實現
Step 1 將真實Activity替換為替身Activity

Activity啟動過程-1
)所以HOOK的地方改為了 android.app.IActivityManager.Stub.Proxy
if ("startActivity".equals(method.getName())) { { // 找到引數裡面的第一個Intent 物件 Intent raw; int index = 0; for (int i = 0; i < args.length; i++) { if (args[i] instanceof Intent) { index = i; break; } } raw = (Intent) args[index]; Intent newIntent = new Intent(); // 這裡包名直接寫死,如果再外掛裡,不同的外掛有不同的包傳遞外掛的包名即可 String targetPackage = "com.xxx.xxx.xx"; // 這裡我們把啟動的Activity臨時替換為 StubActivity ComponentName componentName = new ComponentName(targetPackage, StubActivity.class.getCanonicalName()); newIntent.setComponent(componentName); // 把我們原始要啟動的TargetActivity先存起來 newIntent.putExtra(HookHelper.EXTRA_TARGET_INTENT, raw); // 替換掉Intent, 達到欺騙AMS的目的 args[index] = newIntent; Log.d(TAG, "hook success"); return method.invoke(mBase, args); } return method.invoke(mBase, args);
Step 2 將替身Activity重新換為真實Activity

Activity啟動過程-2
行百里者半九十。現在我們的startActivity啟動一個沒有顯式宣告的Activity已經不會拋異常了,但是要真正正確地把TargetActivity啟動起來,還有一些事情要做。其中最重要的一點是,我們用替身StubActivity臨時換了TargetActivity,肯定需要在『合適的』時候替換回來;接下來我們就完成這個過程。
在AMS程序裡面我們是沒有辦法換回來的,因此我們要等AMS把控制權交給App所在程序,也就是上面那個『Activity啟動過程簡圖』的第三步。AMS程序轉移到App程序也是通過Binder呼叫完成的,承載這個功能的Binder物件是IApplicationThread;在App程序它是Server端,在Server端接受Binder遠端呼叫的是Binder執行緒池,Binder執行緒池通過Handler將訊息轉發給App的主執行緒;(我這裡不厭其煩地敘述Binder呼叫過程,希望讀者不要反感,其一加深印象,其二懂Binder真的很重要)我們可以在這個Handler裡面將替身恢復成真身。
/* package */ class ActivityThreadHandlerCallback implements Handler.Callback { Handler mBase; public ActivityThreadHandlerCallback(Handler base) { mBase = base; } @Override public boolean handleMessage(Message msg) { switch (msg.what) { // ActivityThread裡面 "LAUNCH_ACTIVITY" 這個欄位的值是100 // 本來使用反射的方式獲取最好, 這裡為了簡便直接使用硬編碼 case 100: handleLaunchActivity(msg); break; } mBase.handleMessage(msg); return true; } private void handleLaunchActivity(Message msg) { // 這裡簡單起見,直接取出TargetActivity; Object obj = msg.obj; // 根據原始碼: // 這個物件是 ActivityClientRecord 型別 // 我們修改它的intent欄位為我們原來儲存的即可. /*switch (msg.what) { /case LAUNCH_ACTIVITY: { /Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); /final ActivityClientRecord r = (ActivityClientRecord) msg.obj; / /r.packageInfo = getPackageInfoNoCheck( /r.activityInfo.applicationInfo, r.compatInfo); /handleLaunchActivity(r, null); */ try { // 把替身恢復成真身 Field intent = obj.getClass().getDeclaredField("intent"); intent.setAccessible(true); Intent raw = (Intent) intent.get(obj); Intent target = raw.getParcelableExtra(HookHelper.EXTRA_TARGET_INTENT); raw.setComponent(target.getComponent()); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
這個Callback類的使命很簡單:把替身StubActivity恢復成真身TargetActivity;有了這個自定義的Callback之後我們需要把ActivityThread裡面處理訊息的Handler類H的的mCallback修改為自定義callback類的物件:
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); currentActivityThreadField.setAccessible(true); Object currentActivityThread = currentActivityThreadField.get(null); // 由於ActivityThread一個程序只有一個,我們獲取這個物件的mH Field mHField = activityThreadClass.getDeclaredField("mH"); mHField.setAccessible(true); Handler mH = (Handler) mHField.get(currentActivityThread); // 設定它的回撥, 根據原始碼: // 我們自己給他設定一個回撥,就會替代之前的回撥; //public void dispatchMessage(Message msg) { //if (msg.callback != null) { //handleCallback(msg); //} else { //if (mCallback != null) { //if (mCallback.handleMessage(msg)) { //return; //} //} //handleMessage(msg); //} //} Field mCallBackField = Handler.class.getDeclaredField("mCallback"); mCallBackField.setAccessible(true); mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));
這個時候就大功告成了。
小結
這篇文章只是告訴如何啟動一個未註冊的Activity,只說瞭如何讓它啟動起來,對於它的整個生命週期管理比如destory等的處理方式其實是一樣的。本文重在原理解析。
最後,在本文所述例子中,外掛Activity與替身Activity存在於同一個Apk,因此係統的ClassLoader能夠成功載入並建立TargetActivity的例項。但是在實際的外掛系統中,要啟動的目標Activity肯定存在於一個單獨的apk中,系統預設的ClassLoader無法載入外掛中的Activity類——系統壓根兒就不知道要載入的外掛在哪,談何載入?因此還有一個很重要的問題需要處理: 外掛系統中類的載入 ,解決了『啟動沒有在AndroidManifest.xml中顯式宣告的,並且存在於外部檔案中的Activity』的問題,外掛系統對於Activity的管理才算得上是一個完全體。
不知道有沒有時間填這個坑,希望step 2不會等太久。