Hook簡述以及Hook AMS 實現統一登入
什麼是hook?
hook 翻譯成中文是 名詞鉤子、掛鉤,動詞 鉤住的意思。
在程式中 Hook 是一種技術,也成為鉤子函式。
實際上,一個處理訊息的程式段,通過系統的呼叫,把它掛入系統。 在系統沒有呼叫到該函式之前,鉤子程式先捕獲該訊息,先得到控制權,然後加工處理,然後再扔給系統做後續的處理。
比如說,有一輛貨車 每天從A倉庫拉貨(一個集裝箱的蘋果) 到 B 水果分發站。 我們的鉤子函式的呢,就是在 該貨車拉 集裝箱之前,開啟集裝箱 做一些自己的操作,比如把蘋果換成其他的水果。然後再把集裝箱放回原地,讓貨車拉走。 對於這個過程 貨車是無法感知到的。
上面的例子比較… 但可以幫助理解hook技術。
hook的作用?
在我們開發過程中,有時候某些API 不能滿足我們的需求時,我們就可以使用Hook 來進而改變一個 系統api 的原有功能。
比如通過hook AMS 實現統一登入等。
如何hook?
我們這裡介紹一下 java層的hook 方式。有兩種:
1.利用系統內部提供的介面,通過實現該介面,然後注入自己的操作邏輯 (只在特定的場景下適用)
2.動態代理(所有場景)
如何查詢hook點:
找到hook 點: 靜態的變數或者單例,生命週期跨度較長,不會輕易重新建立。
Hook 過程
- 找到要Hook的點,通過反射或者其他方式 拿到該引用。
- 選擇合適的方式處理邏輯,動態代理 或者 介面方式。
- 用我們修改過的物件,替換原來的物件(即上面提到的,把集裝箱放回原位置,等貨車拉走)
ok,這裡只是簡述一下 hook 相關的概念
Hook AMS 實現應用統一登入
整體思路:
1 . 我們使用動態代理的方式 hook
到 AMS 中的startActivity()
方法。當外部啟動頁面時,比如,啟動購物車頁面(未在AndroidManifest中註冊)時。我們在代理的 invoke
方法中, 替換準備啟動的 Intent
物件為 空殼Activity(已在AndroidManifest中註冊),並把啟動購物車的Intent 傳遞過去, 然後讓系統做後續的啟動操作。
2 . hook
到 ActivityThread 中的 mH(Handler,負責系統訊息的處理) ,並給他設定 CallBack
, 在 callback 中 我們便可以 獲取到啟動 Activity的訊息 LAUNCH_ACTIVITY
即100, 然後我們可以取出 intent 替換為我們想要啟動的購物車介面, 當然可以在其中加入一些判斷登入的邏輯來實現統一的登入校驗.
上面的思路,可能稍微有些抽象,下面我們來上程式碼:
我們先新增一些基本的頁面以及簡單的跳轉操作程式碼:
/**
* 跳轉到購物車介面
* @param view
*/
public void toCartPage(View view) {
startActivity(new Intent(MainActivity.this,CartPageActivity.class));
}
/**
* 跳轉到我的介面
* @param view
*/
public void toMinePage(View view) {
startActivity(new Intent(MainActivity.this,MinePageActivity.class));
}
上面的 CartPageActivity
、 MinePageActivity
介面均未在AndroidManifest.xml 中註冊。
我們需要新增一個 空殼頁面, 這個頁面呢,沒有任何的實際作用,只是為了能讓 未註冊的頁面可以正常的通過系統的檢查。
public class ProxyActivity extends Activity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_proxy);
}
}
ok,下面我們來上 hook 部分的程式碼。
/**
* hook ams 中的 startActivity 方法,代理該方法,新增我們自己的操作,然後再拋給 系統做後續的流程
*/
public void hookStartActivity() {
try {
Class<?> mManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
//hook到 gDefault 這個單例 Singleton
Field mIActivityManagerSington = mManagerNativeClazz.getDeclaredField("gDefault");
mIActivityManagerSington.setAccessible(true);
Object gDefaultValue = mIActivityManagerSington.get(null);
Class<?> mSingletonClazz = Class.forName("android.util.Singleton");
Field mSingletonField = mSingletonClazz.getDeclaredField("mInstance");
mSingletonField.setAccessible(true);
//通過反射的方式 獲取到 我們的 IActivityManager 引用
Object iActivityManagerObj = mSingletonField.get(gDefaultValue);
CheckStartActivityHandler checkStartActivityHandler = new CheckStartActivityHandler(iActivityManagerObj);
Class<?> mIActivityMangerInterface = Class.forName("android.app.IActivityManager");
// 此處通過動態代理的方式 生成 代理IActivityManager中介面的代理物件 mProxyActivityManager 並設定給了系統
Object mProxyActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mIActivityMangerInterface}, checkStartActivityHandler);
mSingletonField.set(gDefaultValue,mProxyActivityManager);
} catch (Exception e) {
e.printStackTrace();
}
}
我們來說說上面都幹了些啥事:
1.反射的方式獲取到了ActivityManageNative
中的 gDefault
欄位 即 Singleton<IActivityManager>
,它的內部持有IActivityManager
的引用。 我們的 AMS 是 IActivityManager
的具體實現。
2.從 gDefault
取出了我們的 IActivityManager
通過下面這幾行程式碼:
Class<?> mSingletonClazz = Class.forName("android.util.Singleton");
Field mSingletonField = mSingletonClazz.getDeclaredField("mInstance");
mSingletonField.setAccessible(true);
//通過反射的方式 獲取到 我們的 IActivityManager 引用
Object iActivityManagerObj = mSingletonField.get(gDefaultValue);
3.建立了動態代理的Handler裝置,即實現 InvocationHandler
,這是硬性要求。沒的選。同時,我們傳入了我們的 IActivityManager
引用.
CheckStartActivityHandler checkStartActivityHandler = new CheckStartActivityHandler(iActivityManagerObj);
4.動態代理系統 IActivityManager
中的介面,並返回 生成的代理物件 即 mProxyActivityManager
並設定給系統,就是我們說的,我們還得把集裝箱還回去。所有 IActivityManager
中的介面都呼叫 都會被委託到 我們的 checkStartActivityHandler
中, 呼叫 invoke 方法。
Class<?> mIActivityMangerInterface = Class.forName("android.app.IActivityManager");
// 此處通過動態代理的方式 生成 代理IActivityManager中介面的代理物件 mProxyActivityManager 並設定給了系統
Object mProxyActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mIActivityMangerInterface}, checkStartActivityHandler);
mSingletonField.set(gDefaultValue,mProxyActivityManager);
簡單提一下動態代理的幾個引數含義:
loader
:類載入器型別,由哪個類載入器來載入代理物件
interfaces
:一組介面,代理物件會實現該介面。
invocationHandler
:當代理物件呼叫方法時,會關聯到Handler 並呼叫該 invoke
方法。
上面的暫且說到這,我們繼續來看代理中的處理程式碼:
/**
* 動態代理 所需要實現的, 每一個代理的介面方法 都會委託給我們 這個handler 裝置 來進行處理,即都會呼叫到 invoke 方法
*/
class CheckStartActivityHandler implements InvocationHandler {
private Object mOldActivityManager;
public CheckStartActivityHandler(Object iOldActivityManger) {
this.mOldActivityManager = iOldActivityManger;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.e(TAG,"hook method "+method);
if ("startActivity".equals(method.getName())){ //如果方法時 啟動Activity
Intent mIntent = null;
int argIndex = 0;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Intent){
mIntent = (Intent) arg;
argIndex = i;
}
}
Intent newIntent = new Intent();
newIntent.setComponent(new ComponentName(mContext,ProxyActivity.class));
//把startActivity 中的 Intnet 傳遞過去, mIntent 為想要開啟的頁面。
newIntent.putExtra("oldIntent",mIntent);
args[argIndex] = newIntent; // 替換啟動的intent
}
return method.invoke(mOldActivityManager,args);
}
}
說下 invoke
方法中的幾個引數:
proxy
: 生成的代理物件
method
: 我們所要呼叫方法的Method物件
args
: 該Mehod所需要的方法引數
你也看到了,我們只是判斷了方法的名稱, 然後取出對應的 Intent
然後替換為我們的 空殼Activity,這裡只是為了可以通過AMS的相關檢查。 然後把我們的 想要啟動的頁面傳遞過去(比如購物車介面,注意,購物車介面並未註冊)。
第一步的操作到這裡就完成了。我們也可以歇一會,讓 Activity 先飛一會。
接下來,我們要進行第二步的操作,即通過回撥的方式 hook 系統的 訊息處理Handler。
/**
* 通過設定回撥的方式, hook M 即訊息Handler 判斷是否是啟動Activity 的訊息 即 100;
*/
public void hookMHandler(){
try {
Class<?> mActivityThreadClazz = Class.forName("android.app.ActivityThread");
Method currentActivityThread = mActivityThreadClazz.getDeclaredMethod("currentActivityThread");
Object mActivityThreadInstance = currentActivityThread.invoke(null);
Field mHandlerField = mActivityThreadClazz.getDeclaredField("mH");
mHandlerField.setAccessible(true);
Handler mActivityThreadHandler = (Handler) mHandlerField.get(mActivityThreadInstance);
Field mHandlerCallBackField = Handler.class.getDeclaredField("mCallback");
mHandlerCallBackField.setAccessible(true);
mHandlerCallBackField.set(mActivityThreadHandler,new HandlerCallBack(mActivityThreadHandler));
} catch (Exception e) {
e.printStackTrace();
}
}
上面做了3件事:
1.反射獲取到 ActivityThread
即 mActivityThreadInstance
2.獲取 ActivityThread
內部的訊息處理Handler mH
,並獲取其內部的 Field域 mCallback
3.給該欄位,設定Callback.即下面這段程式碼:
mHandlerCallBackField.set(mActivityThreadHandler,new HandlerCallBack(mActivityThreadHandler));
下面我們貼上CallBack 內部的處理程式碼:
class HandlerCallBack implements Handler.Callback {
private Handler mActivityThreadH;
public HandlerCallBack(Handler mActivityThreadHandler) {
this.mActivityThreadH = mActivityThreadHandler;
}
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 100){ // 當前是正在 啟動Activity 以及過了 AMS的相關檢測
launchRealActivity(msg);
}
mActivityThreadH.handleMessage(msg);
return true;
}
private void launchRealActivity(Message msg) {
// 此處的 obj 為 ActivityClientRecord 物件 儲存了 Activity 啟動的資訊
Object mActivityRecordObj = msg.obj;
try {
Field mIntentField = mActivityRecordObj.getClass().getDeclaredField("intent");
mIntentField.setAccessible(true);
// 此處我們獲取到的是 啟動Activity 時的 代理intent
Intent mProxyIntent = (Intent) mIntentField.get(mActivityRecordObj);
Intent mOldIntent = mProxyIntent.getParcelableExtra("oldIntent");
//TODO: 此處判斷登入的邏輯,如果未登入 則跳轉到登入介面。否則 開啟傳遞的 Intent 即 mOldIntent;
mProxyIntent.setComponent(mOldIntent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
判斷訊息是否是啟動Activity,如果是,則從Intent中取出我們真正要啟動的 Activity(比如購物車頁面)。然後啟動即可以了。正如你所看到的 TODO ,我們可以在這裡加入登入校驗的邏輯。
最後,我們只需要在 Application
中呼叫即可:
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
LoginHookCheckUtil hookCheckUtil = new LoginHookCheckUtil(this);
hookCheckUtil.hookStartActivity();
hookCheckUtil.hookMHandler();
}
}
到這裡呢,就結束了。
注意
有一個問題,我目前尚未處理, 仍在尋找解決方式。即 CartPageActivity
、 MinePageActivity
只能繼承 Activity
. 如果繼承 AppCompatActivity
會報錯。 請注意~~~~!!!
程式碼地址: