手把手講解 Android Hook入門Demo

前言

手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細(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().mOnClickListener

public 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ā)之路漫漫長,與眾君共勉!


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容