作者:享學課堂終身VIP周周
轉載請聲明出處!
引子
Hook技術在Android開發領域算是一項黑科技,那么一個新的概念進入視線,我們最關心的3個問題就是,它是什么,有什么用,怎么用本系列將由淺入深 手把手講解這三大問題本文是第一篇, 入門篇
正文大綱
一. hook的定義二. 實用價值三. 前置技能四. Hook通用思路五. 案例實戰六. 效果展示
Demo地址
https://github.com/18598925736/OnClickListenerHookDemo
正文
一. hook的定義
hook,鉤子。勾住系統的程序邏輯。在某段 SDK源碼邏輯執行的過程中,通過代碼手段攔截執行該邏輯,加入自己的代碼邏輯。
二. 實用價值
hook是中級開發通往高級開發的必經之路。如果把谷歌比喻成 安卓的造物主,那么安卓SDK源碼里面就包含了萬事萬物的本源。中級開發者,只在利用萬事萬物,浮于表層,而高級開發者能從本源上去改變萬事萬物,深入核心。最有用的實用價值:hook是安卓面向切面(AOP)編程的基礎,可以讓我們在 不變更原有業務的前提下,插入 額外的邏輯. 這樣,既保護了原有業務的完整性,又能讓額外的代碼邏輯不與原有業務產生耦合. (想象一下,讓你在一個成熟的App上面給 每一個按鈕添加埋點接口,不說一萬個,就說 成百上千個控件讓你埋點,讓你寫 一千次埋點調用,你是不是要崩潰, hook可以輕松實現)學好了hook,就有希望成為高級工程師, 完成初中級無法完成的開發任務, 升職,加薪,出任CEO,迎娶白富美,走上人生巔峰,夠不夠實用?
三. 前置技能
- JAVA反射 熟練掌握類 Class,方法Method,成員Field的使用方法源碼內部,很多類和方法都是 @hide的,外部直接無法訪問,所以只能通過反射,去創建源碼中的類,方法,或者成員.
- 閱讀安卓源碼的能力 hook的切入點都在源碼內部,不能閱讀源碼,不能理清源碼邏輯,則不用談 hook. 其實使用 androidStudio來閱讀源碼有個坑,,有時候會看到源碼里面 "一片飄紅",看似是有什么東西沒有引用進來,其實是因為有部分源碼沒有對開發者開放,解決起來很麻煩, 所以,推薦從安卓官網下載整套源碼,然后使用 SourceInsight 查看源碼。如果不需要跳來跳去的話,直接用 安卓源碼網站 一步到位
四. Hook通用思路
無論多么復雜的源碼,我們想要干涉其中的一些執行流程,最終的 殺招只有一個: “偷梁換柱”. 而 “偷梁換柱”的思路,通常都是一個套路:
1. 根據需求確定 要hook的對象2. 尋找要hook的對象的持有者,拿到要hook的對象 (持有:B類 的成員變量里有 一個是A的對象,那么B就是A的持有者,如下
class B{ A a; } class A{}
3. 定義“要hook的對象”的代理類,并且創建該類的對象4. 使用上一步創建出來的對象,替換掉要hook的對象
上面的4個步驟可能還是有點抽象,那么,下面用一個案例,詳細說明每一個步驟.
五. 案例實戰
這是一個最簡單的案例:我們自己的代碼里面,給一個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的對象的持有者,拿到要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,都會保留原有的源碼邏輯. 另外提一句:當我們要創建的代理類,是被接口所約束的時候,比如現在,我們創建的 ProxyOnClickListenerimplementsView.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內 到這里,最后一步,也完成了.
六. 效果展示
先給出Demo:GithubDemo當我點擊這個 hello World:
彈出一個 Toast,并且:在日志中可以看到
同時我并沒有改動 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的作用,是 用我們自己創建的點擊事件代理對象,替換掉之前的點擊事件。
</pre>
ok,目的達成 v.setOnClickListener已經被 hook.
前方有坑,高能提示:我曾經嘗試,是不是可以將上面兩段代碼換個順序.結果證明,換了之后,hook就不管用了,原因是,hook方法的作用,是將v已有的點擊事件,替換成我們代理的點擊事件。所以,在v還沒有點擊事件的時候進行hook,是沒用的
結語
Hook的水很深,這個只是一個入門級的案例,我寫這個,目的是說明hook技術的套路,不管我們要hook源碼的哪一段邏輯,都逃不過 hook通用思路 這“三板斧”,套路掌握了,就有能力學習更難的Hook技術.Hook的學習,需要我們大量地閱讀源碼,要對SDK有較為深入的了解,再也不是浮于表面,只會對SDK的api進行調用,而是真正地干涉“造物主谷歌”的既定規則. 學習難度很大,但是收益也不小,高級開發和初中級開發的薪資差距巨大,職場競爭力也不可同日而語.
你的贊和關注是我繼續創作的動力~