手把手講解 Android Hook入門Demo
前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到儘量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫原始碼的追蹤,原始碼截圖,繪製類的結構圖,儘量詳細地解釋原理的探索過程
3、提供Github 的 可執行的Demo工程,但是我所提供程式碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的執行過程中的注意事項
5、用gif圖,最直觀地展示demo執行效果
如果覺得細節太細,直接跳過看結論即可。
本人能力有限,如若發現描述不當之處,歡迎留言批評指正。
學到老活到老,路漫漫其修遠兮。與眾君共勉 !
引子
我之前的一鍵換膚技術文章裡面提到了hook技術的概念,有讀者反饋說看不太懂, 看來,還是沒有說"人話",其實可以描述地再接地氣一點,於是再寫一片專文吧。
本文只做入門級引子,旨在讓不瞭解 Hook的人通過本文,能認識到 hook 有什麼用
, 怎麼用
, 怎麼學
,能達到這個目的,我就滿足了.
正文大綱
1. hook的定義
2. 實用價值
3. 前置技能
4. hook通用思路
5. 案例實戰
6. 效果展示
正文
1. hook的定義
hook,鉤子。勾住系統的程式邏輯。
在某段 SDK原始碼邏輯
執行的過程中,通過程式碼手段 攔截
執行該邏輯,加入 自己
的程式碼邏輯。
2. 實用價值
hook是中級開發通往高階開發的必經之路。
如果把谷歌比喻成 安卓的造物主,那麼安卓SDK原始碼裡面就包含了萬事萬物的本源。
中級開發者,只在利用萬事萬物,浮於表層,而高階開發者能從本源上去改變萬事萬物,深入核心。
最有用的實用價值:
hook是安卓面向切面(AOP)程式設計的基礎,可以讓我們在 不變更原有業務的前提
下,插入 額外的邏輯
.
這樣,既保護了原有業務的完整性,又能讓額外的程式碼邏輯不與原有業務產生耦合.
(想象一下,讓你在一個成熟的app上面給 每一個
按鈕新增埋點介面,不說一萬個,就說 成百上千個
控制元件讓你埋點,讓你寫 一千次
埋點呼叫,你是不是要崩潰, hook
可以輕鬆實現)
學好了hook,就有希望成為高階工程師,
完成初中級無法完成的開發任務,
升職,加薪,出任CEO,迎娶白富美,走上人生巔峰,夠不夠實用?
3. 前置技能
-
java反射熟練掌握類
Class,方法Method,成員Field
的使用方法原始碼內部,很多類和方法都是
@hide
的,外部直接無法訪問,所以只能通過反射,去建立原始碼中的類,方法,或者成員.
-
閱讀安卓原始碼的能力
hook
的切入點都在原始碼內部,不能閱讀原始碼,不能理清原始碼邏輯,則不用談hook
.其實使用
androidStudio
來閱讀原始碼有個坑,,有時候會看到原始碼裡面"一片飄紅"
,看似是有什麼東西沒有引用進來,解決起來很麻煩,所以,推薦從安卓官網下載整套原始碼,然後使用SourceInsight
檢視原始碼。
4. hook通用思路
無論多麼複雜的原始碼,我們想要干涉其中的一些執行流程,最終的 殺招
只有一個: “偷樑換柱”
.
而 “偷樑換柱”
的思路,通常都是一個套路:
1. 根據需求確定 要hook的物件
2. 尋找要hook的物件的持有者
(持有:B類 的成員變數裡有 一個是A的物件,那麼B就是A的持有者,如下)
class B{ A a; } class A{}
3. 定義“要hook的物件”的代理類,並且建立該類的物件
4. 使用上一步創建出來的物件,替換掉要hook的物件
上面的4個步驟可能還是有點抽象,那麼,下面用一個案例,詳細說明每一個步驟.
5. 案例實戰
這是一個最簡單的案例:
我們自己的程式碼裡面,給一個view設定了點選事件,現在要求在不改動這個點選事件的情況下,新增額外的點選事件邏輯.
View v = findViewById(R.id.tv); v.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "別點啦,再點我咬你了...", Toast.LENGTH_SHORT).show(); } });
這是 view
的點選事件, toast
了一段話,現在要求,不允許改動這個 OnClickListener
,要在 toast
之前新增日誌列印 Log.d(...)
.
乍一看,無從下手.看 hook
如何解決.
按照上面的思路來:
第一步:根據需求確定 要hook的物件;
我們的目的是在 OnClickListener
中,插入自己的邏輯.所以,確定要 hook
的,是 v.setOnClickListener()
方法的實參。
第二步:尋找要hook的物件的持有者
進入 v.setOnClickListener
原始碼:發現我們建立的 OnClickListener
物件被賦值給了getListenerInfo().mOnClickListener
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
繼續索引: getListenerInfo()
是個什麼玩意?繼續追查:
ListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; }
結果發現這個其實是一個 偽單例
,一個View物件中只存在一個 ListenerInfo
物件.
進入ListenerInfo內部:發現 OnClickListener
物件 被ListenerInfo所持有.
static class ListenerInfo { ... public OnClickListener mOnClickListener; ... }
到這裡為止,完成第二步,找到了點選事件的實際持有者: ListenerInfo
.
第三步:定義“要 hook
的物件”的代理類,並且建立該類的物件
我們要 hook
的是 View.OnClickListener
物件,所以,建立一個類 實現 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", "點選事件被hook到了"); if (oriLis != null) { oriLis.onClick(v); } } }
然後, new
出它的物件待用。
ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
可以看到,這裡傳入了一個 View.OnClickListener
物件,它存在的目的,是讓我們可以有選擇地使用到原先的點選事件邏輯。一般 hook
,都會保留原有的原始碼邏輯.
另外提一句:當我們要建立的代理類,是被介面所約束的時候,比如現在,我們建立的 ProxyOnClickListener implements View.OnClickListener
,只實現了一個介面,則可以使用JDK提供的Proxy類來建立代理物件
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", "點選事件被hook到了");//加入自己的邏輯 return method.invoke(onClickListenerInstance, args);//執行被代理的物件的邏輯 } });
這個 代理類
並不是此次的重點,所以一筆帶過.
到這裡為止,第三步: 定義“要hook的物件”的代理類,並且建立該類的物件
完成。
第四步:使用上一步創建出來的物件,替換掉要hook的物件 ,達成 偷樑換柱
的最終目的.
利用反射,將我們建立的代理點選事件物件,傳給這個view
field.set(mListenerInfo, proxyOnClickListener);
這裡,貼出最終程式碼:
/** * hook的輔助類 * hook的動作放在這裡 */ public class HookSetOnClickListenerHelper { /** * hook的核心程式碼 * 這個方法的唯一目的:用自己的點選事件,替換掉 View原來的點選事件 * * @param v hook的範圍僅限於這個view */ public static void hook(Context context, final View v) {// try { // 反射執行View類的getListenerInfo()方法,拿到v的mListenerInfo物件,這個物件就是點選事件的持有者 Method method = View.class.getDeclaredMethod("getListenerInfo"); method.setAccessible(true);//由於getListenerInfo()方法並不是public的,所以要加這個程式碼來保證訪問許可權 Object mListenerInfo = method.invoke(v);//這裡拿到的就是mListenerInfo物件,也就是點選事件的持有者 //要從這裡面拿到當前的點選事件物件 Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 這是內部類的表示方法 Field field = listenerInfoClz.getDeclaredField("mOnClickListener"); final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真實的mOnClickListener物件 //2. 建立我們自己的點選事件代理類 //方式1:自己建立代理類 //ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance); //方式2:由於View.OnClickListener是一個介面,所以可以直接用動態代理模式 // Proxy.newProxyInstance的3個引數依次分別是: // 本地的類載入器; // 代理類的物件所繼承的介面(用Class陣列表示,支援多個介面) // 代理類的實際邏輯,封裝在new出來的InvocationHandler內 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", "點選事件被hook到了");//加入自己的邏輯 return method.invoke(onClickListenerInstance, args);//執行被代理的物件的邏輯 } }); //3. 用我們自己的點選事件代理類,設定到"持有者"中 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", "點選事件被hook到了"); if (oriLis != null) { oriLis.onClick(v); } } } }
這段程式碼閱讀起來的可能難點:
-
Method,Class,Field
的使用
method.setAccessible(true);
//由於getListenerInfo()
方法並不是public
的,所以要加這個程式碼來保證訪問許可權
field.set(mListenerInfo, proxyOnClickListener);
//把一個proxyOnClickListener
物件,設定給mListenerInfo
物件的field
屬性. -
Proxy.newProxyInstance
的使用
Proxy.newProxyInstance
的3個引數依次分別是:
本地的類載入器;
代理類的物件所繼承的介面(用Class陣列表示,支援多個介面)
代理類的實際邏輯,封裝在new出來的InvocationHandler
內
到這裡,最後一步,也完成了.
6. 效果展示
先給出Demo: GithubDemo
當我點選這個 hello World :

image.png
Toast
,並且:在日誌中可以看到

image.png
setOnClickListener
的程式碼,我只是在它的後面,加了一行
HookSetOnClickListenerHelper.hook(this, v);
v.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "別點啦,再點我咬你了...", Toast.LENGTH_SHORT).show(); } }); HookSetOnClickListenerHelper.hook(this, v);//這個hook的作用,是 用我們自己建立的點選事件代理物件,替換掉之前的點選事件。
ok,目的達成 v.setOnClickListener
已經被 hook
.
前方有坑,高能提示:
我曾經嘗試,是不是可以將上面兩段程式碼換個順序. 結果證明,換了之後,hook就不管用了,原因是,hook方法的作用,是將v已有的 點選事件,替換成 我們代理的點選事件。所以,在v還沒有點選事件的時候進行hook,是沒用的
結語
Hook的水很深,這個只是一個入門級的案例,我寫這個,目的是說明hook技術的套路,不管我們要hook原始碼的哪一段邏輯,都逃不過 hook通用思路 這“三板斧”,套路掌握了,就有能力學習更難的Hook技術.
Hook的學習,需要我們大量地閱讀原始碼,要對SDK有較為深入的瞭解,再也不是浮於表面,只會對SDK的api進行呼叫,而是真正地干涉“造物主谷歌”的既定規則. 學習難度很大,但是收益也不小,高階開發和初中級開發的薪資差距巨大,職場競爭力也不可同日而語.