外掛化知識梳理(10) Service 外掛化實現及原理

澤毛發表於2017-12-21

一、Service 外掛化思路

很可惜,Small不支援Service的外掛化,但是在專案中我們確實有這樣的需求,那麼就需要研究一下如何自己來實現Service的外掛化。在討論如何實現Service的外掛化之前,必須有三點準備:

外掛化知識梳理(6) - Small 原始碼分析之 Hook 原理 這篇文章中,我們一起學習瞭如何實現Activity的外掛化,簡單地來說,實現原理就是:

  • 在呼叫startActivity啟動外掛Activity後,通過替換mInstrumentation成員變數,攔截這一啟動過程,在後臺偷偷地把startActivity時傳入的intent中的component替換成為在AndroidManifest.xml中預先註冊的佔坑Activity,再通知ActivityManagerService
  • ActivityManagerService完成排程後,有替換客戶端中的ActivityThread中的mH中的mCallback,將佔坑的Activity重新恢復成外掛的Activity

Service的外掛化也可以採用類似的方式,大體的思路如下:

  • 在呼叫startService啟動外掛Service時,通過攔截ActivityManagerProxy的對應方法,將Intent中的外掛Service型別替換成預先在AndroidManifest.xml中預先註冊好的佔坑Service
  • AMS通過ApplicationThreadProxy回撥佔坑Service對應的生命週期時,我們再在佔坑Service中的onStartCommand中,去建立外掛Service的例項,如果是第一次建立,那麼先呼叫它的onCreate方法,再呼叫它的onStartCommand方法,否則,就只呼叫onStartCommand方法就可以了。
  • 在呼叫stopService停止外掛Service時,同樣通過攔截ActivityManagerProxy的對應方法,去呼叫外掛ServiceonDestroy,如果此時發現沒有任何一個與佔坑Service關聯的外掛Service執行時,那麼就可以停止外掛Service了。

二、具體實現

傳了一個簡單的例子到倉庫,大家可以簡單地對照著看一下,下面,我們開始分析具體的實現。

2.1 準備工作

初始化的過程如下所示:

    public void setup(Context context) {
        try {
            //1.通過反射獲取到ActivityManagerNative類。
            Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
            Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);
            Object gDefault = gDefaultField.get(activityManagerNativeClass);

            //2.獲取mInstance變數。
            Class<?> singleton = Class.forName("android.util.Singleton");
            Field instanceField = singleton.getDeclaredField("mInstance");
            instanceField.setAccessible(true);

            //3.獲取原始的物件。
            Object original = instanceField.get(gDefault);

            //4.動態代理,用於攔截Intent。
            Class<?> iActivityManager = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{ iActivityManager }, new IActivityManagerInvocationHandler(original));
            instanceField.set(gDefault, proxy);

            //5.讀取外掛當中的Service。
            loadService();

            //6.佔坑的Component。
            mStubComponentName = new ComponentName(ServiceManagerApp.getAppContext().getPackageName(), StubService.class.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

這裡最主要的就是做了兩件事:

2.1.1 對 ActivityManagerNative.getDefault() 呼叫的攔截

這裡應用到了動態代理的知識,我們用Proxy.newProxyInstance所建立的proxy物件,替代了ActivityManagerNative中的gDefault靜態變數。在 Framework 原始碼解析知識梳理(1) - 應用程式與 AMS 的通訊實現 中,我們分析過,它其實是一個AMS在應用程式程式中的代理類。通過這一替換過程,那麼當呼叫ActivityManagerNative.getDefault()方法時,就會先經過IActivityManagerInvocationHandler類,我們就可以根據invoke所傳入的方法名,在方法真正被呼叫之前插入一些自己的邏輯(也就是前文所說的,將啟動外掛ServiceIntent替換成啟動佔坑ServiceIntent),最後才會通過Binder呼叫到達AMS端,以此達到了“欺騙系統”的目的。

下面是InvocationHandler的具體實現,建構函式中傳入的是原始的ActivityManagerProxy物件。

    private class IActivityManagerInvocationHandler implements InvocationHandler {

        private Object mOriginal;


        public IActivityManagerInvocationHandler(Object original) {
            mOriginal = original;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            switch (methodName) {
                case "startService":
                    Intent matchIntent = null;
                    int matchIndex = 0;
                    for (Object object : args) {
                        if (object instanceof Intent) {
                            matchIntent = (Intent) object;
                            break;
                        }
                        matchIndex++;
                    }
                    if (matchIntent != null && ServiceManager.getInstance().isPlugService(matchIntent.getComponent())) {
                        Intent stubIntent = new Intent(matchIntent);
                        stubIntent.setComponent(getStubComponentName());
                        stubIntent.putExtra(KEY_ORIGINAL_INTENT, matchIntent);
                        //將外掛的Service替換成佔坑的Service。
                        args[matchIndex] = stubIntent;
                    }
                    break;
                case "stopService":
                    Intent stubIntent = null;
                    int stubIndex = 0;
                    for (Object object : args) {
                        if (object instanceof Intent) {
                            stubIntent = (Intent) object;
                            break;
                        }
                        stubIndex++;
                    }
                    if (stubIntent != null) {
                        boolean destroy = onStopService(stubIntent);
                        if (destroy) {
                            //如果需要銷燬佔坑的Service,那麼就替換掉Intent進行處理。
                            Intent destroyIntent = new Intent(stubIntent);
                            destroyIntent.setComponent(getStubComponentName());
                            args[stubIndex] = destroyIntent;
                        } else {
                            //由於在onStopService中已經手動呼叫了onDestroy,因此這裡什麼也不需要做,直接返回就可以。
                            return null;
                        }
                    }
                    break;
                default:
                    break;
            }
            Log.d("ServiceManager", "call invoke, methodName=" + method.getName());
            return method.invoke(mOriginal, args);
        }
    }
複製程式碼

先看startServiceargs引數中儲存了startService所傳入的實參,這裡面就包含了啟動外掛ServiceIntent,我們將目標IntentComponent替換成為佔坑的Component,然後將原始的Intent儲存在KEY_ORIGINAL_INTENT欄位當中,最後,通過原始的物件呼叫到ActivityManagerService端。

2.1.2 載入外掛 Service 類

這裡其實就是用到了前面介紹的DexClassLoader的知識,詳細的可以看一下前面的這兩篇文章 外掛化知識梳理(7) - 類的動態載入入門外掛化知識梳理(8) - 類的動態載入原始碼分析。 最終,我們會將外掛ServiceClass物件儲存在一個mLoadServicesMap當中,它的Key就是外掛Service的包名和類名。

    private void loadService() {
        try {
            //從外掛中載入Service類。
            File dexOutputDir = ServiceManagerApp.getAppContext().getDir("dex2", 0);
            String dexPath = Environment.getExternalStorageDirectory().toString() + PLUG_SERVICE_PATH;
            DexClassLoader loader = new DexClassLoader(dexPath, dexOutputDir.getAbsolutePath(), null, ServiceManagerApp.getAppContext().getClassLoader());
            try {
                Class clz = loader.loadClass(PLUG_SERVICE_NAME);
                mLoadedServices.put(new ComponentName(PLUG_SERVICE_PKG, PLUG_SERVICE_NAME), clz);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

2.2 啟動外掛 Service

接下來,在宿主中通過下面的方式啟動外掛Service類:

    public void startService(View view) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(ServiceManager.PLUG_SERVICE_PKG, ServiceManager.PLUG_SERVICE_NAME));
        startService(intent);
    }
複製程式碼

按照前面的分析,首先會走到我們預設的“陷阱”當中,可以看到,這裡面的Intent還是外掛ServiceComponent名字:

外掛化知識梳理(10)   Service 外掛化實現及原理
然而,經過替換,最終呼叫時的Intent就變成了佔坑的Service
外掛化知識梳理(10)   Service 外掛化實現及原理
如果一切正常,接下來佔坑Service就會啟動,依次呼叫它的onCreateonStartCommand方法,我們在onStartCommand中,再去回撥外掛Service對應的生命週期:

public class StubService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("StubService", "onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("StubService", "onStartCommand");
        ServiceManager.getInstance().onStartCommand(intent, flags, startId);
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        ServiceManager.getInstance().onDestroy();
        Log.d("StubService", "onDestroy");
    }
}
複製程式碼

這裡,我們取出之前儲存在KEY_ORIGINAL_INTENT中原始的Intent,通過它找到對應外掛Service的包名和類名,以此為key,在mLoadedServices中找到前面從外掛apk中載入的Service類,並通過反射例項化該物件,如果是第一次建立,那麼先執行它的onCreate方法,並將它儲存在mAliveServices中,之後再執行它的onStartCommand方法。

外掛化知識梳理(10)   Service 外掛化實現及原理
這一過程的列印如下圖所示:
外掛化知識梳理(10)   Service 外掛化實現及原理

2.3 停止外掛 Service

當我們通過stopService方法,停止外掛Service時,也會和前面類似,先走到攔截的邏輯當中:

外掛化知識梳理(10)   Service 外掛化實現及原理
而在onStop方法中,我們判斷它是否是需要停止外掛Service,如果是那麼就呼叫外掛ServiceonDestory()方法,並且判斷與佔坑Service相關聯的外掛Service是否都已經結束了,如果是,那麼就返回true,讓佔坑Service也銷燬。
外掛化知識梳理(10)   Service 外掛化實現及原理
銷燬的時候,就是將外掛ServiceIntent替換成佔坑Service
外掛化知識梳理(10)   Service 外掛化實現及原理
這時的列印為:
外掛化知識梳理(10)   Service 外掛化實現及原理

三、總結

以上,就是實現外掛化Service的核心思路,實現起來並不簡單,需要涉及到很多的知識,這已經是外掛化學習的第十篇文章了。如果大家能一路看下來,可以發現,其實外掛化並沒有什麼神祕的地方,如果我們希望實現任意一個元件的外掛化,無非就是以下幾點:

  • 元件的生命週期。
  • 元件的啟動過程,最主要就是和ActivityManagerService的互動過程,這也是最難的地方,要花很多的時間去看原始碼,而且各個版本的API也可能有所差異。
  • 外掛化常用技巧,也就是Hook,動態代理之類的知識。
  • 類動態載入的知識。

掌握了以上幾點,對於市面上大廠的外掛框架基本能夠看懂個六七成,但是對於大多數人而言,並沒有這麼多的時間和條件,去分析一些細節問題。我寫的這些文章,也只能算是入門水平,和大家一起學習基本的思想。真正核心的東西,還是需要有機會能應用到生產環境中才能真正掌握。

很可惜,我也沒有這樣的機會,感覺每天工作的時間都是在調UI、解Bug、浪費時間,只能靠著晚上的時間,一點點摸索,寫Demo,哎,說出來都是淚,還有半年,繼續加油吧!


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章