8個類,1500行代碼搞定插件化 原 薦

寫在前面

本文原創,轉載請以鏈接形式註明地址:http://kymjs.com/code/2016/05/22/01

動態加載一個 Service 到應用中,同樣採用的是和 Activity 一樣的僞裝欺騙系統識別的方案。

接上一篇:8個類搞定插件化——Activity實現方案

本篇主要介紹 Android 插件化開發中,如何運行未安裝apk中的 Service。同我兩年前講過的那種方案(運行未安裝apk中的Service)不同,這次實現的方案是完全沒有任何限制的,插件 apk 可以是一個完全獨立的應用,而不需要做特殊語法修改。

Android插件化

Service 加載過程

同 Activity 的動態加載原理一樣,最首先需要講講 Service 的啓動與加載過程。主要流程如下圖:

Android插件化2

Service 的啓動與 Activity 類似,最終都會調用到ActivityManagerService裏面的方法。

  • 然後是startServiceLocked()安全監測;
  • 安全校驗完成以後,scheduleCreateService()準備創建Service
  • 再調用scheduleServiceArgs()發消息
  • 最終會在 ActivityThread.Callback中處理Handle發送的消息。

明確Service啓動的一整套流程後,發現儘管與前一篇講的Activity的啓動流程非常相似,但是不能用 Activity 的那種做法了,因爲完全沒有用到Instrumentation這個類。
而且跟 Activity 裏一樣,我們也沒辦法覆蓋掉校驗方法startServiceLocked() 來打到篡改系統校驗的目的,因爲它運行在另一個系統進程system_server中。
最後還有一個問題就是,Service 不同於 Activity 可以啓動多個實例,同一個 Service 如果執行過後,是不會再次調用 onCreate()方法的。

替換系統的 Service 創建過程

儘管沒有辦法通過Instrumentation來創建Service但我們依然有辦法替換掉系統創建過程。
首先找到 service 對象是從哪裏new出來的,查看源碼知道,在最後的那步ActivityThread.CallbackHandle發送了衆多的消息類型,其中包括:CREATE_SERVICE、SERVICE_ARGS、BIND_SERVICE 等等…… 不僅是 service 的創建,連 Activity 的生命週期方法也是在這個回調中調用的。
在 CREATE_SERVICE 這個消息中,調用了一個叫handleCreateService(CreateServiceData data)的方法,其中主要代碼爲:

private void handleCreateService(CreateServiceData data) {  
	LoadedApk packageInfo = getPackageInfoNoCheck(data.info.applicationInfo, data.compatInfo); 
	Service service = null;
	try {
	 java.lang.ClassLoader cl = packageInfo.getClassLoader();
	service = (Service) cl.loadClass(data.info.name).newInstance();
	} catch (Exception e) {
	}
	
	Application app = packageInfo.makeApplication(false, mInstrumentation);
    service.attach(context, this, data.info.name, data.token, app, ActivityManagerNative.getDefault());
    service.onCreate();
    mServices.put(data.token, service);
    try {
    ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, 0, 0, 0);
    } catch (RemoteException e) {
    }
}

可以看到,其實Service也是一個普通的類,在這裏就是系統new出來並執行了他的onCreate()方法。
所以我們就可以通過替換掉這個 callback 類,並修改其邏輯如果是 CREATE_SERVICE 這條消息,就執行我們自己的Service創建邏輯。
而我們自己的邏輯,就通過判斷,如果正在加載的 service 是一個插件 service 就替換ClassLoader爲插件 classloader,加載出來的類一切照原宿主service的流程走一遍,包括那些attach()onCreate()方法,都手動調用一遍。
替換方法依舊是通過反射,找到原本ActivityThread類中的mH這個類。

Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(currentActivityThread);

Field mCallBackField = Handler.class.getDeclaredField("mCallback");
mCallBackField.setAccessible(true);

//修改它的callback爲我們的,從而HOOK掉
ActivityThreadHandlerCallback callback = new ActivityThreadHandlerCallback(mH);
mCallBackField.set(mH, callback);

真的是要吐槽一下 Android 源碼,裏面充斥着各種奇葩命名,比如這個 mH,它其實是一個 Handle,但是它的類名就一個字母,一個大寫的 H,所以他的對象叫 mH。 然後還有,前一個 ActivityInfo 類型的變量叫 aInfo,後面又出現一個 ApplicationInfo 的對象也叫 aInfo,然後時不時還來個 ai,你也不知道到底是啥還得再翻回去找它的類型。
OK,回正題,替換完 callback 後,創建 Service 就可以由我們自己的方法來執行了。但是還有一個問題,就是onCreate不會多次調用的問題,因此我們同時還要修改handleMessage()的邏輯,如果是 SERVICE_ARGS 或者 BIND_SERVICE 這兩個消息,則首先進行一次判斷,如果傳入的插件 service 是個沒有創建過的,那麼就需要再次運行handleCreateService()方法去創建一次。

@Override
public boolean handleMessage(Message msg) {
    switch (msg.what) {
    case 114: //CREATE_SERVICE
        if (!handleCreateService(msg.obj)) {
            mOldHandle.handleMessage(msg);
        }
        break;
    case 115: //SERVICE_ARGS
        handleBindService(msg.obj);
        mOldHandle.handleMessage(msg);
        break;
    case 121: //BIND_SERVICE
        handleBindService(msg.obj);
        mOldHandle.handleMessage(msg);
        break;
    }
    return true;
}

/**
 * startService時調用,如果插件Service是首次啓動,則首先執行創建
 *
 * @param data BindServiceData對象
 */
private void handleBindService(Object data) {
    ServiceStruct struct = pluginServiceMap.get(IActivityManagerHook.currentClazzName);
    //如果這個插件service沒有啓動過
    if (struct == null) {
        //本來這裏應該是傳一個CreateServiceData對象,但是由於本方法實際使用的只有CreateServiceData.token
        //這個token在BindServiceData以及ServiceArgsData中有同名field,所以這裏偷個懶,直接傳遞了
        handleCreateService(data);
    }
}

踩坑與爬坑

如果你照着上面的思路實現了整個插件化,你會發現其實還有兩個巨大的坑:

  • 插件 service 雖然創建了,但是如果啓動了多個插件 service,那麼除了最後一次啓動的那個 service,其他插件 service 的onCreate()以外的其他生命週期方法一個都沒有調用。
  • 插件 service 不會調用onDestroy()方法。

首先解決第一個問題,生命週期方法。之前說過,每個生命週期方法其實也是通過這個 handle 來處理的。找到相應的消息事件:SERVICE_ARGS、BIND_SERVICE、STOP_SERVICE,發現這三個事件調用的方法都有一句共同的代碼:Service s = mServices.get(data.token);
原來所有創建過的 service 都會被加入到一個 map 中(這個 map 在 4.0 以前是HashMap,4.0 以後是ArrayMap),在需要使用的時候就從這個 map 中根據 key 也就是 token 對象來讀取,如果讀不到,就不會調用生命週期方法了。
再翻回之前的 service 創建的代碼handleCreateService(),那句mServices.put(data.token, service);原來就是做這個用的。同樣也解釋了爲什麼其他 service 不會調用生命週期方法了,因爲 map 的值都被覆蓋了嘛。 那麼簡單,這個 key 值 token 我們自己來創建並加入到裏面就行了。

第二個坑,onDestroy() 不執行,經過反覆測試,發現實際上問題在於帶有 STOP_SERVICE 標識的消息就沒有被髮出,具體原因不得而知,猜測可能是安全校驗沒通過。解決的辦法也很簡單,既然系統沒有發出,那麼就手動發送一次這個消息就行了。
找到一切消息發送的源頭——ActivityManagerService,那麼非常簡單,通過通過動態代理,就可以替換掉我們關注的方法了。
找到 destroy 相關的兩個方法,名字叫:stopServiceToken()unbindService()。在這兩個方法執行的時候,調用一下doServiceDestroy()自己去手動發一下消息。然後在另一邊接收的時候接收到這個消息就執行插件的onDestroy()

public void doServiceDestroy() {
    Message msg = mH.obtainMessage();
    msg.what = ActivityThreadHandlerCallback.PLUGIN_STOP_SERVICE;
    mH.sendMessage(msg);
}

private void handleCreateService(CreateServiceData data) {  
	switch (msg.what) {
    case 116: //STOP_SERVICE
    case PLUGIN_STOP_SERVICE:
        if (!handleStopService()) {
            mOldHandle.handleMessage(msg);
        }
        break;
    }
    return true;
} 

/**
 * destroy策略,如果是最後一個service,則停止真實Service
 */
private boolean handleStopService() {
    ServiceStruct struct = pluginServiceMap.get(IActivityManagerHook.currentClazzName);
    if (struct != null) {
        pluginServiceMap.remove(IActivityManagerHook.currentClazzName);
        if (pluginServiceMap.size() == 0) {
            return false;
        } else {
            struct.service.onDestroy();
            return true;
        }
    }
    return false;
}

資源與so文件動態加載

這樣,動態加載未安裝APK中的ActivityService就都解決了,回顧一下,總共就只需要6個類就夠了,那麼爲什麼說是8個類搞定插件化呢,因爲還有兩類是用來處理資源和 so 文件的動態加載的。
先說 so 文件,其實DexClassLoader原生就支持動態加載的,但是爲什麼我們傳入的 solib 並沒有加載出來呢,還是因爲權限。在 Android 手機上的 SD 卡是不具備可執行權限的,所以我們必須將 so 文件複製到應用包內存儲區域,不管是getFilesDir()或者是getCacheDir()都是具有可執行權限的目錄,在構造插件DexClassLoader的時候,第三個參數傳入具有可執行權限的路徑就可以了。
資源的話就更簡單了,由於我們只需要動態加載一個 apk,所以完全涉及不到插件資源衝突問題,只需要一個方法:

public void loadRes(Context context, String resPath) throws Exception {
    assetManager = AssetManager.class.newInstance();
    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
    addAssetPath.invoke(assetManager, resPath);
    //在插件的Activity中替換掉原本的resource就可以了
    resources = new Resources(assetManager, context.getResources().getDisplayMetrics(),
            context.getResources().getConfiguration());
}

結尾

結尾沒有花絮~
就這麼簡單8個類,難道你還有什麼疑問嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章