引子
Hook技術(shù)在android開發(fā)領(lǐng)域算是一項(xiàng)黑科技做粤,那么一個(gè)新的概念進(jìn)入視線抬伺,我們最關(guān)心的3個(gè)問(wèn)題就是怎顾,它是什么读慎,有什么用,怎么用
本系列將由淺入深 手把手講解這三大問(wèn)題
本文是第一篇槐雾,
入門篇
正文大綱
一. hook的定義
二. 實(shí)用價(jià)值
三. 前置技能
四. Hook通用思路
五. 案例實(shí)戰(zhàn)
六. 效果展示
Demo地址
https://github.com/18598925736/OnClickListenerHookDemo
正文
一. hook的定義
hook夭委,鉤子。勾住系統(tǒng)的程序邏輯募强。
在某段SDK源碼邏輯
執(zhí)行的過(guò)程中株灸,通過(guò)代碼手段攔截
執(zhí)行該邏輯,加入自己
的代碼邏輯擎值。
二. 實(shí)用價(jià)值
hook是中級(jí)開發(fā)通往高級(jí)開發(fā)的必經(jīng)之路慌烧。
如果把谷歌比喻成 安卓的造物主,那么安卓SDK源碼里面就包含了萬(wàn)事萬(wàn)物的本源鸠儿。
中級(jí)開發(fā)者杏死,只在利用萬(wàn)事萬(wàn)物,浮于表層捆交,而高級(jí)開發(fā)者能從本源上去改變?nèi)f事萬(wàn)物,深入核心腐巢。
最有用的實(shí)用價(jià)值:
hook是安卓面向切面(AOP)編程的基礎(chǔ),可以讓我們?cè)?code>不變更原有業(yè)務(wù)的前提下品追,插入額外的邏輯
.
這樣,既保護(hù)了原有業(yè)務(wù)的完整性冯丙,又能讓額外的代碼邏輯不與原有業(yè)務(wù)產(chǎn)生耦合.
(想象一下肉瓦,讓你在一個(gè)成熟的app上面給每一個(gè)
按鈕添加埋點(diǎn)接口遭京,不說(shuō)一萬(wàn)個(gè),就說(shuō)成百上千個(gè)
控件讓你埋點(diǎn),讓你寫一千次
埋點(diǎn)調(diào)用泞莉,你是不是要崩潰哪雕,hook
可以輕松實(shí)現(xiàn))
學(xué)好了hook,就有希望成為高級(jí)工程師,
完成初中級(jí)無(wú)法完成的開發(fā)任務(wù),
升職,加薪,出任CEO鲫趁,迎娶白富美斯嚎,走上人生巔峰,夠不夠?qū)嵱茫?/strong>
三. 前置技能
- java反射 熟練掌握類
Class,方法Method挨厚,成員Field
的使用方法
源碼內(nèi)部堡僻,很多類和方法都是@hide
的,外部直接無(wú)法訪問(wèn),所以只能通過(guò)反射疫剃,去創(chuàng)建源碼中的類钉疫,方法,或者成員.
- 閱讀安卓源碼的能力
hook
的切入點(diǎn)都在源碼內(nèi)部巢价,不能閱讀源碼牲阁,不能理清源碼邏輯,則不用談hook
.
其實(shí)使用androidStudio
來(lái)閱讀源碼有個(gè)坑壤躲,城菊,有時(shí)候會(huì)看到源碼里面"一片飄紅"
,看似是有什么東西沒有引用進(jìn)來(lái)柒爵,其實(shí)是因?yàn)橛胁糠衷创a沒有對(duì)開發(fā)者開放役电,解決起來(lái)很麻煩,
所以棉胀,推薦從安卓官網(wǎng)下載整套源碼法瑟,然后使用SourceInsight
查看源碼。
如果不需要跳來(lái)跳去的話唁奢,直接用 安卓源碼網(wǎng)站 一步到位
四. Hook通用思路
無(wú)論多么復(fù)雜的源碼霎挟,我們想要干涉其中的一些執(zhí)行流程,最終的殺招
只有一個(gè): “偷梁換柱”
.
而 “偷梁換柱”
的思路麻掸,通常都是一個(gè)套路:
1. 根據(jù)需求確定 要hook的對(duì)象
2. 尋找要hook的對(duì)象的持有者酥夭,拿到要hook的對(duì)象
(持有:B類 的成員變量里有 一個(gè)是A的對(duì)象,那么B就是A的持有者,如下)class B{ A a; } class A{}
3. 定義“要hook的對(duì)象”的代理類脊奋,并且創(chuàng)建該類的對(duì)象
4. 使用上一步創(chuàng)建出來(lái)的對(duì)象熬北,替換掉要hook的對(duì)象
上面的4個(gè)步驟可能還是有點(diǎn)抽象,那么,下面用一個(gè)案例诚隙,詳細(xì)說(shuō)明每一個(gè)步驟.
五. 案例實(shí)戰(zhàn)
這是一個(gè)最簡(jiǎn)單的案例:
我們自己的代碼里面讶隐,給一個(gè)view設(shè)置了點(diǎn)擊事件,現(xiàn)在要求在不改動(dòng)這個(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)在要求,不允許改動(dòng)這個(gè)OnClickListener
,要在toast
之前添加日志打印 Log.d(...)
.
乍一看炉峰,無(wú)從下手.看hook
如何解決.
按照上面的思路來(lái):
第一步:根據(jù)需求確定 要hook的對(duì)象畏妖;
我們的目的是在OnClickListener
中,插入自己的邏輯.所以疼阔,確定要hook
的戒劫,是v.setOnClickListener()
方法的實(shí)參。
第二步:尋找要hook的對(duì)象的持有者竿开,拿到要hook的對(duì)象
進(jìn)入v.setOnClickListener
源碼:發(fā)現(xiàn)我們創(chuàng)建的OnClickListener
對(duì)象被賦值給了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對(duì)象中只存在一個(gè)ListenerInfo
對(duì)象.
進(jìn)入ListenerInfo內(nèi)部:發(fā)現(xiàn)OnClickListener
對(duì)象 被ListenerInfo所持有.static class ListenerInfo { ... public OnClickListener mOnClickListener; ... }
到這里為止否彩,完成第二步疯攒,找到了點(diǎn)擊事件的實(shí)際持有者:
ListenerInfo
.
第三步:定義“要
hook
的對(duì)象”的代理類,并且創(chuàng)建該類的對(duì)象
我們要hook
的是View.OnClickListener
對(duì)象列荔,所以敬尺,創(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
出它的對(duì)象待用贴浙。ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
可以看到砂吞,這里傳入了一個(gè)
View.OnClickListener
對(duì)象,它存在的目的,是讓我們可以有選擇地使用到原先的點(diǎn)擊事件邏輯崎溃。一般hook
蜻直,都會(huì)保留原有的源碼邏輯.
另外提一句:當(dāng)我們要?jiǎng)?chuàng)建的代理類,是被接口所約束的時(shí)候袁串,比如現(xiàn)在概而,我們創(chuàng)建的ProxyOnClickListener implements View.OnClickListener
,只實(shí)現(xiàn)了一個(gè)接口囱修,則可以使用JDK提供的Proxy類來(lái)創(chuàng)建代理對(duì)象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í)行被代理的對(duì)象的邏輯 } });
這個(gè)
代理類
并不是此次的重點(diǎn)赎瑰,所以一筆帶過(guò).
到這里為止,第三步:定義“要hook的對(duì)象”的代理類破镰,并且創(chuàng)建該類的對(duì)象
完成餐曼。
第四步:使用上一步創(chuàng)建出來(lái)的對(duì)象,替換掉要hook的對(duì)象鲜漩,達(dá)成
偷梁換柱
的最終目的.
利用反射源譬,將我們創(chuàng)建的代理點(diǎn)擊事件對(duì)象,傳給這個(gè)view
field.set(mListenerInfo, proxyOnClickListener);
這里孕似,貼出最終代碼:
/**
* hook的輔助類
* hook的動(dòng)作放在這里
*/
public class HookSetOnClickListenerHelper {
/**
* hook的核心代碼
* 這個(gè)方法的唯一目的:用自己的點(diǎn)擊事件瓶佳,替換掉 View原來(lái)的點(diǎn)擊事件
*
* @param v hook的范圍僅限于這個(gè)view
*/
public static void hook(Context context, final View v) {//
try {
// 反射執(zhí)行View類的getListenerInfo()方法,拿到v的mListenerInfo對(duì)象鳞青,這個(gè)對(duì)象就是點(diǎn)擊事件的持有者
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);//由于getListenerInfo()方法并不是public的霸饲,所以要加這個(gè)代碼來(lái)保證訪問(wèn)權(quán)限
Object mListenerInfo = method.invoke(v);//這里拿到的就是mListenerInfo對(duì)象,也就是點(diǎn)擊事件的持有者
//要從這里面拿到當(dāng)前的點(diǎn)擊事件對(duì)象
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對(duì)象
//2. 創(chuàng)建我們自己的點(diǎn)擊事件代理類
// 方式1:自己創(chuàng)建代理類
// ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
// 方式2:由于View.OnClickListener是一個(gè)接口臂拓,所以可以直接用動(dòng)態(tài)代理模式
// Proxy.newProxyInstance的3個(gè)參數(shù)依次分別是:
// 本地的類加載器;
// 代理類的對(duì)象所繼承的接口(用Class數(shù)組表示厚脉,支持多個(gè)接口)
// 代理類的實(shí)際邏輯,封裝在new出來(lái)的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í)行被代理的對(duì)象的邏輯
}
});
//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);
}
}
}
}
這段代碼閱讀起來(lái)的可能難點(diǎn):
Method,Class,Field
的使用
method.setAccessible(true);
//由于getListenerInfo()
方法并不是public
的傻工,所以要加這個(gè)代碼來(lái)保證訪問(wèn)權(quán)限
field.set(mListenerInfo, proxyOnClickListener);
//把一個(gè)proxyOnClickListener
對(duì)象,設(shè)置給mListenerInfo
對(duì)象的field
屬性.
Proxy.newProxyInstance
的使用
Proxy.newProxyInstance
的3個(gè)參數(shù)依次分別是:
本地的類加載器;
代理類的對(duì)象所繼承的接口(用Class數(shù)組表示孵滞,支持多個(gè)接口)
代理類的實(shí)際邏輯中捆,封裝在new出來(lái)的InvocationHandler
內(nèi)
到這里,最后一步坊饶,也完成了.
六. 效果展示
先給出Demo:GithubDemo
當(dāng)我點(diǎn)擊這個(gè) hello World:
彈出一個(gè)Toast
泄伪,并且:在日志中可以看到
同時(shí)我并沒有改動(dòng)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)擊事件代理對(duì)象算撮,替換掉之前的點(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é)語(yǔ)
Hook的水很深须教,這個(gè)只是一個(gè)入門級(jí)的案例皿渗,我寫這個(gè),目的是說(shuō)明hook技術(shù)的套路轻腺,不管我們要hook源碼的哪一段邏輯乐疆,都逃不過(guò) hook通用思路 這“三板斧”,套路掌握了贬养,就有能力學(xué)習(xí)更難的Hook技術(shù).
Hook的學(xué)習(xí)挤土,需要我們大量地閱讀源碼,要對(duì)SDK有較為深入的了解误算,再也不是浮于表面仰美,只會(huì)對(duì)SDK的api進(jìn)行調(diào)用迷殿,而是真正地干涉“造物主谷歌”的既定規(guī)則. 學(xué)習(xí)難度很大,但是收益也不小咖杂,高級(jí)開發(fā)和初中級(jí)開發(fā)的薪資差距巨大庆寺,職場(chǎng)競(jìng)爭(zhēng)力也不可同日而語(yǔ).
高級(jí)開發(fā)之路漫漫長(zhǎng),與眾君共勉!