前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細(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)兮。與眾君共勉 !
引子
我之前的一鍵換膚技術(shù)文章里面提到了hook技術(shù)的概念,有讀者反饋說看不太懂, 看來,還是沒有說"人話",其實(shí)可以描述地再接地氣一點(diǎn),于是再寫一片專文吧。
本文只做入門級引子,旨在讓不了解 Hook的人通過本文,能認(rèn)識到 hook
有什么用,怎么用,怎么學(xué),能達(dá)到這個(gè)目的,我就滿足了.
正文大綱
1. hook的定義
2. 實(shí)用價(jià)值
3. 前置技能
4. hook通用思路
5. 案例實(shí)戰(zhàn)
6. 效果展示
正文
1. hook的定義
hook,鉤子。勾住系統(tǒng)的程序邏輯。
在某段SDK源碼邏輯執(zhí)行的過程中,通過代碼手段攔截執(zhí)行該邏輯,加入自己的代碼邏輯。
2. 實(shí)用價(jià)值
hook是中級開發(fā)通往高級開發(fā)的必經(jīng)之路。
如果把谷歌比喻成 安卓的造物主,那么安卓SDK源碼里面就包含了萬事萬物的本源。
中級開發(fā)者,只在利用萬事萬物,浮于表層,而高級開發(fā)者能從本源上去改變?nèi)f事萬物,深入核心。
最有用的實(shí)用價(jià)值:
hook是安卓面向切面(AOP)編程的基礎(chǔ),可以讓我們在不變更原有業(yè)務(wù)的前提下,插入額外的邏輯.
這樣,既保護(hù)了原有業(yè)務(wù)的完整性,又能讓額外的代碼邏輯不與原有業(yè)務(wù)產(chǎn)生耦合.
(想象一下,讓你在一個(gè)成熟的app上面給每一個(gè)按鈕添加埋點(diǎn)接口,不說一萬個(gè),就說成百上千個(gè)控件讓你埋點(diǎn),讓你寫一千次埋點(diǎn)調(diào)用,你是不是要崩潰,hook可以輕松實(shí)現(xiàn))
學(xué)好了hook,就有希望成為高級工程師,
完成初中級無法完成的開發(fā)任務(wù),
升職,加薪,出任CEO,迎娶白富美,走上人生巔峰,夠不夠?qū)嵱茫?/strong>
3. 前置技能
- java反射 熟練掌握類
Class,方法Method,成員Field的使用方法
源碼內(nèi)部,很多類和方法都是@hide的,外部直接無法訪問,所以只能通過反射,去創(chuàng)建源碼中的類,方法,或者成員.
- 閱讀安卓源碼的能力
hook的切入點(diǎn)都在源碼內(nèi)部,不能閱讀源碼,不能理清源碼邏輯,則不用談hook.
其實(shí)使用androidStudio來閱讀源碼有個(gè)坑,,有時(shí)候會看到源碼里面"一片飄紅",看似是有什么東西沒有引用進(jìn)來,其實(shí)是因?yàn)橛胁糠衷创a沒有對開發(fā)者開放,解決起來很麻煩,
所以,推薦從安卓官網(wǎng)下載整套源碼,然后使用SourceInsight查看源碼。
如果不需要跳來跳去的話,直接用 安卓源碼網(wǎng)站 一步到位
4. hook通用思路
無論多么復(fù)雜的源碼,我們想要干涉其中的一些執(zhí)行流程,最終的殺招只有一個(gè): “偷梁換柱”.
而 “偷梁換柱”的思路,通常都是一個(gè)套路:
1. 根據(jù)需求確定 要hook的對象
2. 尋找要hook的對象的持有者,拿到要hook的對象
(持有:B類 的成員變量里有 一個(gè)是A的對象,那么B就是A的持有者,如下)class B{ A a; } class A{}3. 定義“要hook的對象”的代理類,并且創(chuàng)建該類的對象
4. 使用上一步創(chuàng)建出來的對象,替換掉要hook的對象
上面的4個(gè)步驟可能還是有點(diǎn)抽象,那么,下面用一個(gè)案例,詳細(xì)說明每一個(gè)步驟.
5. 案例實(shí)戰(zhàn)
這是一個(gè)最簡單的案例:
我們自己的代碼里面,給一個(gè)view設(shè)置了點(diǎn)擊事件,現(xiàn)在要求在不改動這個(gè)點(diǎn)擊事件的情況下,添加額外的點(diǎn)擊事件邏輯.
View v = findViewById(R.id.tv);
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "別點(diǎn)啦,再點(diǎn)我咬你了...", Toast.LENGTH_SHORT).show();
}
});
這是view的點(diǎn)擊事件,toast了一段話,現(xiàn)在要求,不允許改動這個(gè)OnClickListener,要在toast之前添加日志打印 Log.d(...).
乍一看,無從下手.看hook如何解決.
按照上面的思路來:
第一步:根據(jù)需求確定 要hook的對象;
我們的目的是在OnClickListener中,插入自己的邏輯.所以,確定要hook的,是v.setOnClickListener()方法的實(shí)參。
第二步:尋找要hook的對象的持有者,拿到要hook的對象
進(jìn)入v.setOnClickListener源碼:發(fā)現(xiàn)我們創(chuàng)建的OnClickListener對象被賦值給了getListenerInfo().mOnClickListenerpublic void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }繼續(xù)索引:
getListenerInfo()是個(gè)什么玩意?繼續(xù)追查:ListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; }結(jié)果發(fā)現(xiàn)這個(gè)其實(shí)是一個(gè)
偽單例,一個(gè)View對象中只存在一個(gè)ListenerInfo對象.
進(jìn)入ListenerInfo內(nèi)部:發(fā)現(xiàn)OnClickListener對象 被ListenerInfo所持有.static class ListenerInfo { ... public OnClickListener mOnClickListener; ... }到這里為止,完成第二步,找到了點(diǎn)擊事件的實(shí)際持有者:
ListenerInfo.
第三步:定義“要
hook的對象”的代理類,并且創(chuàng)建該類的對象
我們要hook的是View.OnClickListener對象,所以,創(chuàng)建一個(gè)類 實(shí)現(xiàn)View.OnClickListener接口.static class ProxyOnClickListener implements View.OnClickListener { View.OnClickListener oriLis; public ProxyOnClickListener(View.OnClickListener oriLis) { this.oriLis = oriLis; } @Override public void onClick(View v) { Log.d("HookSetOnClickListener", "點(diǎn)擊事件被hook到了"); if (oriLis != null) { oriLis.onClick(v); } } }然后,
new出它的對象待用。ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);可以看到,這里傳入了一個(gè)
View.OnClickListener對象,它存在的目的,是讓我們可以有選擇地使用到原先的點(diǎn)擊事件邏輯。一般hook,都會保留原有的源碼邏輯.
另外提一句:當(dāng)我們要創(chuàng)建的代理類,是被接口所約束的時(shí)候,比如現(xiàn)在,我們創(chuàng)建的ProxyOnClickListener implements View.OnClickListener,只實(shí)現(xiàn)了一個(gè)接口,則可以使用JDK提供的Proxy類來創(chuàng)建代理對象Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]>>{View.OnClickListener.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.d("HookSetOnClickListener", "點(diǎn)擊事件被hook到了");//加入自己的邏輯 return method.invoke(onClickListenerInstance, args);//執(zhí)行被代理的對象的邏輯 } });這個(gè)
代理類并不是此次的重點(diǎn),所以一筆帶過.
到這里為止,第三步:定義“要hook的對象”的代理類,并且創(chuàng)建該類的對象完成。
第四步:使用上一步創(chuàng)建出來的對象,替換掉要hook的對象,達(dá)成
偷梁換柱的最終目的.
利用反射,將我們創(chuàng)建的代理點(diǎn)擊事件對象,傳給這個(gè)view
field.set(mListenerInfo, proxyOnClickListener);這里,貼出最終代碼:
/**
* hook的輔助類
* hook的動作放在這里
*/
public class HookSetOnClickListenerHelper {
/**
* hook的核心代碼
* 這個(gè)方法的唯一目的:用自己的點(diǎn)擊事件,替換掉 View原來的點(diǎn)擊事件
*
* @param v hook的范圍僅限于這個(gè)view
*/
public static void hook(Context context, final View v) {//
try {
// 反射執(zhí)行View類的getListenerInfo()方法,拿到v的mListenerInfo對象,這個(gè)對象就是點(diǎn)擊事件的持有者
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加這個(gè)代碼來保證訪問權(quán)限
Object mListenerInfo = method.invoke(v);//這里拿到的就是mListenerInfo對象,也就是點(diǎn)擊事件的持有者
//要從這里面拿到當(dāng)前的點(diǎn)擊事件對象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 這是內(nèi)部類的表示方法
Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真實(shí)的mOnClickListener對象
//2. 創(chuàng)建我們自己的點(diǎn)擊事件代理類
// 方式1:自己創(chuàng)建代理類
// ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
// 方式2:由于View.OnClickListener是一個(gè)接口,所以可以直接用動態(tài)代理模式
// Proxy.newProxyInstance的3個(gè)參數(shù)依次分別是:
// 本地的類加載器;
// 代理類的對象所繼承的接口(用Class數(shù)組表示,支持多個(gè)接口)
// 代理類的實(shí)際邏輯,封裝在new出來的InvocationHandler內(nèi)
Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("HookSetOnClickListener", "點(diǎn)擊事件被hook到了");//加入自己的邏輯
return method.invoke(onClickListenerInstance, args);//執(zhí)行被代理的對象的邏輯
}
});
//3. 用我們自己的點(diǎn)擊事件代理類,設(shè)置到"持有者"中
field.set(mListenerInfo, proxyOnClickListener);
//完成
} catch (Exception e) {
e.printStackTrace();
}
}
// 還真是這樣,自定義代理類
static class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener oriLis;
public ProxyOnClickListener(View.OnClickListener oriLis) {
this.oriLis = oriLis;
}
@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "點(diǎn)擊事件被hook到了");
if (oriLis != null) {
oriLis.onClick(v);
}
}
}
}
這段代碼閱讀起來的可能難點(diǎn):
Method,Class,Field的使用
method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加這個(gè)代碼來保證訪問權(quán)限
field.set(mListenerInfo, proxyOnClickListener);//把一個(gè)proxyOnClickListener對象,設(shè)置給mListenerInfo對象的field屬性.Proxy.newProxyInstance的使用
Proxy.newProxyInstance的3個(gè)參數(shù)依次分別是:
本地的類加載器;
代理類的對象所繼承的接口(用Class數(shù)組表示,支持多個(gè)接口)
代理類的實(shí)際邏輯,封裝在new出來的InvocationHandler內(nèi)
到這里,最后一步,也完成了.
6. 效果展示
先給出Demo:GithubDemo
當(dāng)我點(diǎn)擊這個(gè) hello World:
image.png
彈出一個(gè)Toast,并且:在日志中可以看到image.png
同時(shí)我并沒有改動setOnClickListener的代碼,我只是在它的后面,加了一行HookSetOnClickListenerHelper.hook(this, v);
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "別點(diǎn)啦,再點(diǎn)我咬你了...", Toast.LENGTH_SHORT).show();
}
});
HookSetOnClickListenerHelper.hook(this, v);//這個(gè)hook的作用,是 用我們自己創(chuàng)建的點(diǎn)擊事件代理對象,替換掉之前的點(diǎn)擊事件。
ok,目的達(dá)成v.setOnClickListener已經(jīng)被hook.
前方有坑,高能提示:
我曾經(jīng)嘗試,是不是可以將上面兩段代碼換個(gè)順序. 結(jié)果證明,換了之后,hook就不管用了,原因是,hook方法的作用,是將v已有的 點(diǎn)擊事件,替換成 我們代理的點(diǎn)擊事件。所以,在v還沒有點(diǎn)擊事件的時(shí)候進(jìn)行hook,是沒用的
結(jié)語
Hook的水很深,這個(gè)只是一個(gè)入門級的案例,我寫這個(gè),目的是說明hook技術(shù)的套路,不管我們要hook源碼的哪一段邏輯,都逃不過 hook通用思路 這“三板斧”,套路掌握了,就有能力學(xué)習(xí)更難的Hook技術(shù).
Hook的學(xué)習(xí),需要我們大量地閱讀源碼,要對SDK有較為深入的了解,再也不是浮于表面,只會對SDK的api進(jìn)行調(diào)用,而是真正地干涉“造物主谷歌”的既定規(guī)則. 學(xué)習(xí)難度很大,但是收益也不小,高級開發(fā)和初中級開發(fā)的薪資差距巨大,職場競爭力也不可同日而語.
高級開發(fā)之路漫漫長,與眾君共勉!

