寫在前面
本文原創,轉載請以鏈接形式註明地址:http://kymjs.com/code/2016/05/22/01
動態加載一個 Service 到應用中,同樣採用的是和 Activity 一樣的僞裝欺騙系統識別的方案。
本篇主要介紹 Android 插件化開發中,如何運行未安裝apk中的 Service。同我兩年前講過的那種方案(運行未安裝apk中的Service)不同,這次實現的方案是完全沒有任何限制的,插件 apk 可以是一個完全獨立的應用,而不需要做特殊語法修改。
Service 加載過程
同 Activity 的動態加載原理一樣,最首先需要講講 Service 的啓動與加載過程。主要流程如下圖:
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.Callback
中Handle
發送了衆多的消息類型,其中包括: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中的Activity
和 Service
就都解決了,回顧一下,總共就只需要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個類,難道你還有什麼疑問嗎?