前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細(xì),但是這是為了幫助到盡量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時(shí)候.
這個(gè)系列的文章:
1、用通俗易懂的講解方式,講解一門技術(shù)的實(shí)用價(jià)值
2、詳細(xì)書寫源碼的追蹤,源碼截圖,繪制類的結(jié)構(gòu)圖,盡量詳細(xì)地解釋原理的探索過程
3、提供Github 的 可運(yùn)行的Demo工程,但是我所提供代碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的運(yùn)行過程中的注意事項(xiàng)
5、用gif圖,最直觀地展示demo運(yùn)行效果如果覺得細(xì)節(jié)太細(xì),直接跳過看結(jié)論即可。
本人能力有限,如若發(fā)現(xiàn)描述不當(dāng)之處,歡迎留言批評指正。
學(xué)到老活到老,路漫漫其修遠(yuǎn)兮。與眾君共勉 !
引子
上一篇文章手把手講解 Android Hook入門Demo 中,用了一個(gè)
最最簡單
的案例 講解hook是個(gè)什么玩意. 咱不能老玩低端,來點(diǎn)復(fù)雜的吧。Activity的啟動流程
,做安卓開發(fā)的人都是繞不開它的,但是要真正知悉其源碼邏輯,還是不太容易.
先給出本文的代碼Demo,有興趣的大神們可以下載看看
鳴謝
翻了很多關(guān)于
hook Activity
啟動流程的博客,這位大佬的文章給我的啟發(fā)最大
https://blog.csdn.net/gdutxiaoxu/article/details/81459910
但是,可能大佬的博文對于有些基礎(chǔ)不足的初中級安卓工程師還不夠友好,所以我把大佬的思想用更通俗,更具象化的方式再展示一遍.并且,閱讀源碼的時(shí)候一些坑,我都會詳細(xì)給出解決方案。
正文大綱
1. 兩種啟動Activity的方式源碼追蹤 示例代碼,程序執(zhí)行走向圖.
2. 第一種啟動方式的hook方案
3. 第二種啟動方式的hook方案
4. 目前方案弊端分析
5. 最終解決方案
6. HOOK開發(fā)可能的坑
正文
1. 兩種啟動Activity的方式源碼追蹤 (源碼基于
SDK 28 ~ android-9.0
)
方式1:使用Activity
自帶的startActivity
示例代碼
private void startActivityByActivity() {
Intent i = new Intent(MainActivity.this, Main2Activity.class);
startActivity(i);
}
程序執(zhí)行走向圖.
代碼追蹤:
image.png
image.png
image.png
這里有個(gè)if(mParent==null)
判定,先看true
分支:
發(fā)現(xiàn)一個(gè)坑,mInstrumentation.execStartActivity
這里居然不能繼續(xù)往下索引了?很奇怪,不過不重要,我們直接進(jìn)入Instrumentation.java
去找這個(gè)方法:
image.png
在這個(gè)execStartActivity中,可以找到關(guān)鍵代碼
:int result = ActivityManager.getService() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); checkStartActivityResult(result, intent);
通過這種方式啟動Activity,最終的執(zhí)行權(quán)被交給了 ActivityManager.getService()
(即AMS
),它的作用是 啟動一個(gè)Activity并且返回result
,然后checkStartActivityResult(result, intent);
這句話,對當(dāng)前的跳轉(zhuǎn)意圖intent
進(jìn)行檢測;
image.png
have you declared this activity in your AndroidManifest.xml
這句異常應(yīng)該很熟悉了吧?啟動一個(gè)沒有注冊的Activity的報(bào)錯(cuò).
再看個(gè)if(mParent==null)
的false
分支:
image.png
image.png
控制權(quán)依然是交給了mInstrumentation.execStartActivity()
,剩余的代碼索引和上面的一樣.
所以,代碼索引的結(jié)論,按照一張圖來表示就是:
代碼索引結(jié)論圖1.png
方式2:使用applictonContext
的startActivity
private void startActivityByApplicationContext() {
Intent i = new Intent(MainActivity.this, Main2Activity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(i);
}
在 方式1 中已經(jīng)展示了源碼索引的方式,所以這里不再贅述貼圖.直接給出代碼索引結(jié)論圖
:
兩張圖對比,我們很容易得出一個(gè)結(jié)論:
啟動Activity
的最終執(zhí)行權(quán),都被交給了 Instrumentation.java
類,
方式1:Activity.startActivity
的最終執(zhí)行者是 它的mInstrumentation
成員,mInstrumentation
的持有者是 Activity
自身.
方式2:getApplicationContext().startActivity(i);
的最終執(zhí)行者是:ActivityThread
的 mInstrumentation
成員,持有者是ActivityThread
主線程.
兩種方式都可以把mInstrumentation
當(dāng)作hook切入點(diǎn),將它從它的持有者中"偷梁換柱".
下面開始動手嘗試:
2. 第一種啟動方式的hook方案
創(chuàng)建一個(gè)HookActivityHelper.java
,然后三步走:
- 找到
hook
點(diǎn),以及hook
對象的持有者,上文中已經(jīng)說明:hook
點(diǎn)是Activity
的mInstrumentation
成員,持有者就是Activity
Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation"); mInstrumentationField.setAccessible(true); Instrumentation base = (Instrumentation) mInstrumentationField.get(activity);
base
是系統(tǒng)原來的執(zhí)行邏輯,存起來后面用得著.
- 創(chuàng)建
Instrumentation
代理類, 繼承Instrumentation
然后,重寫execStartActivity
方法,加入自己的邏輯,然后再執(zhí)行系統(tǒng)的邏輯.
private static class ProxyInstrumentation extends Instrumentation {
public ProxyInstrumentation(Instrumentation base) {
this.base = base;
}
Instrumentation base;
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("ProxyInstrumentation", "我們自己的邏輯");
//這里還要執(zhí)行系統(tǒng)的原本邏輯,但是突然發(fā)現(xiàn),這個(gè)execStartActivity居然是hide的,只能反射咯
try {
Class<?> InstrumentationClz = Class.forName("android.app.Instrumentation");
Method execStartActivity = InstrumentationClz.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
return (ActivityResult) execStartActivity.invoke(base,
who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- 用代理類對象替換
hook
對象.
ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base);
mInstrumentationField.set(activity, proxyInstrumentation);
如何使用: 在
MainActivity
的onCreate
中加入一行ActivityHookHelper.hook(this)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityHookHelper.hook(this);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivityByActivity();
}
});
}
private void startActivityByActivity() {
Intent i = new Intent(MainActivity.this, Main2Activity.class);
startActivity(i);
}
}
效果:跳轉(zhuǎn)依然正常,并且logcat
中可以發(fā)現(xiàn)下面的日志.
image.pngok,插入自己的邏輯,成功
3. 第二種啟動方式的hook方案
創(chuàng)建ApplicationContextHookHelper.java
,然后 同樣是三步走
:
1.確定hook的對象和該對象的持有者
鎖定ActivityThread
的mInstrumentation
成員.
//1.主線程ActivityThread內(nèi)部的mInstrumentation對象,先把他拿出來
Class<?> ActivityThreadClz = Class.forName("android.app.ActivityThread");
//再拿到sCurrentActivityThread
Field sCurrentActivityThreadField = ActivityThreadClz.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Object activityThreadObj = sCurrentActivityThreadField.get(null);//靜態(tài)變量的屬性get不需要參數(shù),傳null即可.
//再去拿它的mInstrumentation
Field mInstrumentationField = ActivityThreadClz.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation base = (Instrumentation) mInstrumentationField.get(activityThreadObj);// OK,拿到
2.創(chuàng)建代理對象 和上面的代理類一模一樣,就不重復(fù)貼代碼了
//2.構(gòu)建自己的代理對象,這里Instrumentation是一個(gè)class,而不是接口,所以只能用創(chuàng)建內(nèi)部類的方式來做
ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base);
3.替換掉原對象
//3.偷梁換柱
mInstrumentationField.set(activityThreadObj, proxyInstrumentation);
如何使用: 在
Main4Activity
的onCreate
中加入一行ApplicationContextHookHelper.hook();
public class Main4Activity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main4);
ApplicationContextHookHelper.hook();
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivityByApplicationContext();
}
});
}
private void startActivityByApplicationContext() {
Intent i = new Intent(Main4Activity.this, Main5Activity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(i);
}
}
效果
image.png
OK,第二種啟動方式,我們也可以加入自己的邏輯了.hook成功!
4. 目前方案弊端分析
啟動方式1的hook: 只是在針對單個(gè)Activity
類,來進(jìn)行hook
,多個(gè)Activity
則需要寫多次,或者寫在BaseActivity
里面.
啟動方式2的hook:可以針對全局進(jìn)行hook,無論多少個(gè)Activity,只需要調(diào)用一次ApplicationContextHookHelper.hook();
函數(shù)即可,但是,它只能針對 getApplicationContext().startActivity(i);
普通的Activity.startActivity
則不能起作用.
那么有沒有一種完全體的解決方案:能夠在全局起作用,并且可以在兩種啟動方式下都能hook
.
回顧之前的兩張代碼索引結(jié)論圖
,會發(fā)現(xiàn),兩種啟動Activity的方式,最終都被執(zhí)行到了 AMS
內(nèi)部,
下一步,嘗試hook AMS.
5. 最終解決方案
代碼索引: 基于SDK 28 ~ android9.0
下方紅框標(biāo)記的部分,就是取得
AMS
(ActivityManagerService
實(shí)例)的代碼.
image.png
如果可以在系統(tǒng)接收到AMS實(shí)例之前,把他截
了,是不是就可以達(dá)到我們的目的?
進(jìn)去看看getService的代碼:image.png
真正的AMS
實(shí)例來自一個(gè)Singleton
單例輔助類的create()
方法,并且這個(gè)Singleton
單例類,提供get
方法,獲得真正的實(shí)例.
image.png
那么,我們從這個(gè)單例中,就可以獲得系統(tǒng)當(dāng)前的 AMS
實(shí)例,將它取出來,然后保存.
OK,確認(rèn):
hook
對象: ActivityManager
的IActivityManagerSingleton
成員 變量內(nèi)的 單例 mInstance
.
hook
對象的持有者:ActivityManager
的IActivityManagerSingleton
成員變量
那么,動手:
- 找到
hook
對象,并且存起來
//1.把hook的對象取出來保存
//矮油,靜態(tài)的耶,開心.
Class<?> ActivityManagerClz = Class.forName("android.app.ActivityManager");
Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
final Object IActivityManagerObj = getServiceMethod.invoke(null);//OK,已經(jīng)取得這個(gè)系統(tǒng)自己的AMS實(shí)例
- 創(chuàng)建自己的代理類對象,
IActivityManager
是一個(gè)AIDL生成的動態(tài)接口類,所以在編譯時(shí),androidStudio
會找不到這個(gè)類,所以,先反射,然后用Proxy進(jìn)行創(chuàng)建代理。
//2.現(xiàn)在創(chuàng)建我們的AMS實(shí)例
//由于IActivityManager是一個(gè)接口,那么我們可以使用Proxy類來進(jìn)行代理對象的創(chuàng)建
// 結(jié)果被擺了一道,IActivityManager這玩意居然還是個(gè)AIDL,動態(tài)生成的類,編譯器還不認(rèn)識這個(gè)類,怎么辦?反射咯
Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");
Object proxyIActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IActivityManagerClz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy是創(chuàng)建出來的代理類,method是接口中的方法,args是接口執(zhí)行時(shí)的實(shí)參
if (method.getName().equals("startActivity")) {
Log.d("GlobalActivityHook", "全局hook 到了 startActivity");
}
return method.invoke(IActivityManagerObj, args);
}
});
- 偷梁換柱:這次有點(diǎn)復(fù)雜, 不再是簡單的
field.set
,因?yàn)檫@次的hook對象被包裹在了一個(gè)Singleton
里。
//3.偷梁換柱,這里有點(diǎn)糾結(jié),這個(gè)實(shí)例居然被藏在了一個(gè)單例輔助類里面
Field IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");
IActivityManagerSingletonField.setAccessible(true);
Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
//反射創(chuàng)建一個(gè)Singleton的class
Class<?> SingletonClz = Class.forName("android.util.Singleton");
Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);
使用方法:老樣子,在你自己的Activity onCreate
里面加入GlobalActivityHookHelper.hook();
運(yùn)行起來,預(yù)期結(jié)果應(yīng)該是:能夠在logcat中看到日志 :
GlobalActivityHook - 全局hook 到了 startActivity;
但是,你運(yùn)行起來可能看不到這一行。
如果你看不到這個(gè)日志,那么原因就是:
程序報(bào)錯(cuò)了,
報(bào)錯(cuò)啦!
沒有這樣的方法,怎么回事?
debug
找原因:
image.png
為什么會沒有getService
這個(gè)方法!?
查看了我當(dāng)前設(shè)備的系統(tǒng)版本號image.png
居然是23版本,6.0
.
所以,恍然大悟,我們寫的hook代碼并沒有兼容性,遇到低版本的設(shè)備,就失靈了.
解決方案:
1.找到SDK 23
的源碼
(注意,前方有坑,androidStudio
,你如果直接把combileSDK
改成23.會出現(xiàn)很多位置問題,所以不建議這么做. 但是我們一定要看SDK 23
的源碼,怎么辦?
- .在線查看源碼 - https://www.androidos.net.cn/sourcecode ;
- 從谷歌官網(wǎng)下載
SDK 23
的源碼,然后用SourceInsight
查看)
2.查看getService
方法不存在的原因,兩個(gè)版本28 和 23,在這一塊代碼上有什么不同.
3.改造 GlobalActivityHookHelper.java
,判定當(dāng)前設(shè)備的系統(tǒng)版本號,讓它可以兼容所有版本.
按照上面的步驟:
我發(fā)現(xiàn)SDK 23里面:
Instrumentation
類的 execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, Intent[] intents, Bundle options, int userId)
方法里,獲取AMS實(shí)例
的方式完全不同.
image.png
它是使用ActivityManagerNative.getDefault()
來獲得的,繼續(xù)往下找,看看有沒有什么不同。
進(jìn)去ActivityManagerNative
找找看:
image.png
image.png
OK,找到了區(qū)別,確定結(jié)論:SDK 28
和23
在這塊代碼上的區(qū)別就是:
獲得AMS實(shí)例的類名和方法名都不同.另外,查了度娘之后發(fā)現(xiàn),這個(gè)變化是在SDK 26版本修改的,所以26和26以后,ActivityManager.getService()
來獲取,26以前,用ActivityManagerNative.getDefault()
來獲得
調(diào)整當(dāng)前的hook
方法,修改為下面這樣:
public class GlobalActivityHookHelper {
//設(shè)備系統(tǒng)版本是不是大于等于26
private static boolean ifSdkOverIncluding26() {
int SDK_INT = Build.VERSION.SDK_INT;
if (SDK_INT > 26 || SDK_INT == 26) {
return true;
} else {
return false;
}
}
public static void hook() {
try {
Class<?> ActivityManagerClz;
final Object IActivityManagerObj;
if (ifSdkOverIncluding26()) {
ActivityManagerClz = Class.forName("android.app.ActivityManager");
Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
IActivityManagerObj = getServiceMethod.invoke(null);//OK,已經(jīng)取得這個(gè)系統(tǒng)自己的AMS實(shí)例
} else {
ActivityManagerClz = Class.forName("android.app.ActivityManagerNative");
Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getDefault");
IActivityManagerObj = getServiceMethod.invoke(null);//OK,已經(jīng)取得這個(gè)系統(tǒng)自己的AMS實(shí)例
}
//2.現(xiàn)在創(chuàng)建我們的AMS實(shí)例
//由于IActivityManager是一個(gè)接口,那么其實(shí)我們可以使用Proxy類來進(jìn)行代理對象的創(chuàng)建
// 結(jié)果被擺了一道,IActivityManager這玩意居然還是個(gè)AIDL,動態(tài)生成的類,編譯器還不認(rèn)識這個(gè)類,怎么辦?反射咯
Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");
Object proxyIActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IActivityManagerClz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy是創(chuàng)建出來的代理類,method是接口中的方法,args是接口執(zhí)行時(shí)的實(shí)參
if (method.getName().equals("startActivity")) {
Log.d("GlobalActivityHook", "全局hook 到了 startActivity");
}
return method.invoke(IActivityManagerObj, args);
}
});
//3.偷梁換柱,這里有點(diǎn)糾結(jié),這個(gè)實(shí)例居然被藏在了一個(gè)單例輔助類里面
Field IActivityManagerSingletonField;
if (ifSdkOverIncluding26()) {
IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");
} else {
IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("gDefault");
}
IActivityManagerSingletonField.setAccessible(true);
Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
Class<?> SingletonClz = Class.forName("android.util.Singleton");//反射創(chuàng)建一個(gè)Singleton的class
Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);
} catch (Exception e) {
e.printStackTrace();
}
}
}
再次嘗試:
image.png
成功,實(shí)現(xiàn)了全局范圍內(nèi)的startActivity
動作的hook
.
6. HOOK開發(fā)可能的坑
1. androidStudio
閱讀源碼很多類無法索引,這是因?yàn)橛幸恍╊愂?code>@hide的,無法Ctrl點(diǎn)進(jìn)去,
解決方案:Ctrl+shift+R 輸入類名,手動進(jìn)入.
2.androidStudio
閱讀源碼直接報(bào)紅
:或者一些是AIDL
動態(tài)生成的接口,無法直接查看,比IActivityManager
. ,
解決方案:這種接口不用管它,如果非要用到它,那就使用本類的包名+IActivityManager作為全限定名,去反射創(chuàng)建它.
3. hook
開發(fā),是學(xué)習(xí)源碼思想,改變源碼執(zhí)行流程,所以,在多個(gè)版本的設(shè)備上運(yùn)行,很容易發(fā)生不兼容的情況.
解決方案:找到不兼容的設(shè)備版本,根據(jù)報(bào)的異常,參照源碼的版本變遷做出相應(yīng)的兼容性改動.
結(jié)語
歷時(shí)3天,忙里偷閑,總算是寫完了.
喜歡的客官幫忙點(diǎn)個(gè)贊哦,你們的鼓勵(lì)是我最大的動力,以后還會更新更多干貨.
最后~本文的代碼Demo奉上.