手寫代碼詳解Android Hook入門demo

前言

手把手講解系列文章,是我寫給各位看官,也是寫給我本身的。 文章可能過度詳細,可是這是爲了幫助到儘可能多的人,畢竟工做5,6年,不能老吸血,也到了回饋開源的時候. 這個系列的文章: 一、用通俗易懂的講解方式,講解一門技術的實用價值 二、詳細書寫源碼的追蹤,源碼截圖,繪製類的結構圖,儘可能詳細地解釋原理的探索過程 三、提供Github 的 可運行的Demo工程,可是我所提供代碼,更可能是提供思路,拋磚引玉,請酌情cv 四、集合整理原理探索過程當中的一些坑,或者demo的運行過程當中的注意事項 五、用gif圖,最直觀地展現demo運行效果java

若是以爲細節太細,直接跳過看結論便可。 本人能力有限,如若發現描述不當之處,歡迎留言批評指正。android

學到老活到老,路漫漫其修遠兮。與衆君共勉 !git


引子

我以前的一鍵換膚技術文章裏面提到了hook技術的概念,有讀者反饋說看不太懂, 看來,仍是沒有說"人話",其實能夠描述地再接地氣一點,因而再寫一片專文吧。github

本文只作入門級引子,旨在讓不瞭解 Hook的人經過本文,能認識到 hook有什麼用怎麼用怎麼學,能達到這個目的,我就知足了.編程

正文大綱

1. hook的定義
2. 實用價值
3. 前置技能
4. hook通用思路
5. 案例實戰
6. 效果展現

正文

1. hook的定義

hook,鉤子。勾住系統的程序邏輯。 在某段SDK源碼邏輯執行的過程當中,經過代碼手段攔截執行該邏輯,加入本身的代碼邏輯。api


2. 實用價值

hook是中級開發通往高級開發的必經之路。 若是把谷歌比喻成 安卓的造物主,那麼安卓SDK源碼裏面就包含了萬事萬物的本源。 中級開發者,只在利用萬事萬物,浮於表層,而高級開發者能從本源上去改變萬事萬物,深刻核心。數組

最有用的實用價值: hook是安卓面向切面(AOP)編程的基礎,可讓咱們在不變動原有業務的前提下,插入額外的邏輯. 這樣,既保護了原有業務的完整性,又能讓額外的代碼邏輯不與原有業務產生耦合. (想象一下,讓你在一個成熟的app上面給每個按鈕添加埋點接口,不說一萬個,就說成百上千個控件讓你埋點,讓你寫一千次埋點調用,你是否是要崩潰,hook能夠輕鬆實現)bash

學好了hook,就有但願成爲高級工程師, 完成初中級沒法完成的開發任務, 升職,加薪,出任CEO,迎娶白富美,走上人生巔峯,夠不夠實用?app


3. 前置技能
  • java反射 熟練掌握類Class,方法Method,成員Field的使用方法 源碼內部,不少類和方法都是@hide的,外部直接沒法訪問,因此只能經過反射,去建立源碼中的類,方法,或者成員.
  • 閱讀安卓源碼的能力 hook的切入點都在源碼內部,不能閱讀源碼,不能理清源碼邏輯,則不用談hook. 其實使用 androidStudio來閱讀源碼有個坑,,有時候會看到源碼裏面 "一片飄紅",看似是有什麼東西沒有引用進來,實際上是由於有部分源碼沒有對開發者開放,解決起來很麻煩, 因此,推薦從安卓官網下載整套源碼,而後使用 SourceInsight 查看源碼。 若是不須要跳來跳去的話,直接用 安卓源碼網站 一步到位

4. hook通用思路

不管多麼複雜的源碼,咱們想要干涉其中的一些執行流程,最終的殺招只有一個: 「偷樑換柱」. 而 「偷樑換柱」的思路,一般都是一個套路:ide

1. 根據需求肯定 要hook的對象 2. 尋找要hook的對象的持有者,拿到要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的對象的持有者,拿到要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

彈出一個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的做用,是 用咱們本身建立的點擊事件代理對象,替換掉以前的點擊事件。

複製代碼
ok,目的達成v.setOnClickListener已經被hook.

前方有坑,高能提示我曾經嘗試,是否是能夠將上面兩段代碼換個順序. 結果證實,換了以後,hook就無論用了,緣由是,hook方法的做用,是將v已有的 點擊事件,替換成 咱們代理的點擊事件。因此,在v尚未點擊事件的時候進行hook,是沒用的


結語

Hook的水很深,這個只是一個入門級的案例,我寫這個,目的是說明hook技術的套路,無論咱們要hook源碼的哪一段邏輯,都逃不過 hook通用思路 這「三板斧」,套路掌握了,就有能力學習更難的Hook技術.

Hook的學習,須要咱們大量地閱讀源碼,要對SDK有較爲深刻的瞭解,不再是浮於表面,只會對SDK的api進行調用,而是真正地干涉「造物主谷歌」的既定規則. 學習難度很大,可是收益也不小,高級開發和初中級開發的薪資差距巨大,職場競爭力也不可同日而語.

高級開發之路漫漫長,與衆君共勉!

做者:波瀾步驚 連接:www.jianshu.com/p/74c12164f…

相關文章
相關標籤/搜索