android的hook技術之hook所有view的監聽器

這裏先聲明一下,由於這篇文章早已有人寫過,但是並非盜取他的成果,這裏的雷同確實有些偶然。。。這是做到一半的時候一個同事跟我說網上有,於是乎我看了他的思路以及demo,基本跟我差不多,只是他的代碼寫的可能更好一些,但是並沒有做優化以及各種場景並沒有想到,只是單純的hook技術而已,以下是作者的文章鏈接

如有雷同,純屬巧合吐舌頭


背景: 

       項目中又要求說要監聽所有點擊事件方便上傳數據,所以想了很多辦法最終選定這種既不修改原有代碼又可以完成需求的優雅方式,hook技術;
      隨着熱修復、動態加載等熱門技術的出現,hook技術不再神祕,想必多多少少都已經瞭解,無非就是java的所謂高級反射技術+設計模式的代理模式就可以完成hook。這隻代表個人理解,有誤的地方請不吝指正;

Idea:

      之前有做過換膚功能,當時有個setFactory(),可以能拿到所有的view,當時下意識的想法既然能拿到所有的view,那應該能拿到監聽器,但是仔細一想,這只是剛開始加載view,還沒設置監聽器,怎麼拿都是空的吧。這一想法行不通,只能放棄,setFactory()讓我想到了代理方式,而且最近在拜讀動態加載,所以知道hook技術,靈光就是那麼一閃,下意識的想到,我能不能把整個onClick方法或者onClick的實例給hook了,再結合代理方式,毫無痕跡的入侵了。。。這個想法可以一試,然後就研究onClick的源碼發現View的onclick統一由一個ListenerInfo管理,所以直接hook掉,通過代理方式悄無聲息的實現全面入侵監聽器的功能。

hook時機分析:

      1、首先在現有的項目中基本都有一個BaseActivity,所以當activity加載完佈局,初始化(view的初始化和設置監聽器)完之後,開始hook.
      2、第1只是在加載完layout之後,按照執行流程將所有設置監聽器的view都hook,但是有一種可能就是進入當前頁,初始化完成之後,並沒有立即設置監聽器也沒有更新數據,直到網絡加載完畢,將數據返回後才更新view和設置相應的監聽器;這時需要怎樣才能將最新設置的監聽器hook掉,應該在哪兒監聽這個狀態?這裏賣個關子。。
     3、當listview或者gridview在網絡請求拿到數據之後才設置監聽器,這裏設置監聽器分兩種,一個是listview或gridview設置的監聽器,一個是adapter裏邊的view的監聽器,這個需要怎麼hook?
     4、當listview或者gridview滾動的時候adapter中itemview設置的監聽器怎麼hook,這裏又是一個難點?
     5、自定義的點擊事件。。這裏我就不做特殊處理了,這裏直接手動添加吧,畢竟這種場景少之又少
解決辦法:
    1、對於第一種情況,我們只需要在activity的生命週期中的onWindowFocusChange或者onAttachToWindow()的方法裏直接hook就可以,因爲我們初始化view和設置view的監聽器,一般都在oncreate中執行。而onWindowFocusChange()和onAttachToWindow是在onResume之後,而且onAttachToWindow只執行一次,而onWindowFocusChange()會執行兩次,一次是加載完layout之後window獲取焦點,一次是在要銷燬activity之後window失去焦點。所以一開始會在onAttachToWindow()中hook的,但是後來爲什麼會放在onWindowFocusChange()中hook。。。接下來分析2的時候一起說說
   2、由於第一種只是一部分情況,第2、3中更是一種常態,對於開發者來說很容易寫出2的方式,即請求完數據之後,我才初始化view,並設置相應的監聽器,畢竟有數據我纔會操作頁面,這裏符合用什麼就生成什麼的原則,這時候這個hook的時機在哪兒,由於更新view的時候一般都會重新layout或者onMeasure,所以這裏就查看了view中layout的源碼,發現view中有個addLayoutChangeListener,並且是在layout的時候回調,所以一般在baseActivity可以直接爲rootView設置這個監聽器,但是剛初始化的時候也會多次執行這個方法,所以首次進入的時候要做相應的處理,否則會hook很多次,雖然也做了優化,但是這種hook會頻繁調用。。。畢竟交互是頻繁的,這裏暫時沒想到有什麼比較好的方式,若知道請不吝賜教,這裏先謝了
  3、對於第4種在滾動時候都會重新設置監聽器,而且item是複用的;這裏一般來說項目都有一個統一的Listview,這時我們只需要在onScroll()的時候重新hook一遍listview的所有itemview就好了,但是由於item是複用的可能已經hook過的又重新hook一遍導致會執行兩次hook的監聽器,(比如只滾動一個itemview,那麼只會複用一個itemview,其它itemview還是原來的,這時候都hook,原來的已經hook過了,又hook了一次,這樣就會有兩個hook的監聽器所以會執行2次,周而復始呢,太可怕了。。。)這樣的話數據上報就更加不準了,所以這裏一定要判斷是否已經設置過了

叨咕叨咕這裏就叨咕完了,接下來我們看看實現:上代碼
/**
     * 點擊監聽器
     *
     * @param view
     * @param isScrollAbsListview lsitview或gridView是否滾動:true:滾動則重新hook,false:表示view不是listview或者gridview或者沒滾動
     */
    private void hookOnClickListener(View view, boolean isScrollAbsListview) {
        if (!view.isClickable()) {//默認是不可點擊的,只有設置監聽器纔會設置爲true,證明沒有設置點擊事件或者初始化時沒有設置點擊事件
            Log.d(TAG, "isClickable name = " + view.getClass().getSimpleName());
            return;
        }
        Log.d(TAG, "null != view.getTag(R.id.tag_onclick) = " + (null != view.getTag(R.id.tag_onclick)));
        if (!isScrollAbsListview && null != view.getTag(R.id.tag_onclick)) {//已經hook過,並且不是滾動的listview,不用再hook了
            return;
        }
        try {
            //hook view的信息載體實例listenerInfo:事件監聽器都是這個實例保存的
            Class viewClass = Class.forName("android.view.View");
            Method method = viewClass.getDeclaredMethod("getListenerInfo");
            method.setAccessible(true);
            Object listenerInfoInstance = method.invoke(view);

            //hook信息載體實例listenerInfo的屬性
            Class listenerInfoClass = Class.forName("android.view.View$ListenerInfo");
            Field onClickListerField = listenerInfoClass.getDeclaredField("mOnClickListener");
            onClickListerField.setAccessible(true);
            View.OnClickListener onClickListerObj = (View.OnClickListener) onClickListerField.get(listenerInfoInstance);//獲取已設置過的監聽器
            Log.d(TAG, "onClickListerObj = " + onClickListerObj);
            if (isScrollAbsListview && onClickListerObj instanceof OnClickListenerProxy) {//針對adapterView的滾動item複用會導致重複hook代理監聽器
                return;
            }
            //hook事件,設置自定義的載體事件監聽器
            onClickListerField.set(listenerInfoInstance, new OnClickListenerProxy(onClickListerObj, proxyListenerConfigBuilder.getOnClickProxyListener()));
            setHookedTag(view, R.id.tag_onclick);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
這裏就以hook view的onClick實例爲例,其它長按的以此類推都是相似的;
首先:hook view的listenerInfo實例:
//hook view的信息載體實例listenerInfo:事件監聽器都是這個實例保存的
            Class viewClass = Class.forName("android.view.View");
            Method method = viewClass.getDeclaredMethod("getListenerInfo");
            method.setAccessible(true);
            Object listenerInfoInstance = method.invoke(view);
listenerInfoInstance就是我們聲明view的監聽器的實例了。拿到實例後,要hook實例的屬性字段:
 //hook信息載體實例listenerInfo的屬性
            Class listenerInfoClass = Class.forName("android.view.View$ListenerInfo");
            Field onClickListerField = listenerInfoClass.getDeclaredField("mOnClickListener");
            onClickListerField.setAccessible(true);
            View.OnClickListener onClickListerObj = (View.OnClickListener) onClickListerField.get(listenerInfoInstance);//獲取已設置過的監聽器
onClickObj就是監聽器的實例了。拿到實實在在的監聽器的實例之後我們再通過代理模式將自定義的監聽器代理現有的監聽器:
//hook事件,設置自定義的載體事件監聽器
            onClickListerField.set(listenerInfoInstance, new OnClickListenerProxy(onClickListerObj, proxyListenerConfigBuilder.getOnClickProxyListener()));
這樣基本完成hook.
這裏需要注意的是:瞭解反射技術以及代理技術。反射技術一定要記得子類是無法hook到父類的任何東西的包括繼承的屬性以及方法等等,一定要用父類的包名+類名來反射得到屬性及方法,在通過子類的實例去調用獲取到所需要的字段屬性 
如此例子:我們通過
Class.forName("android.view.View");
而不是通過
Class.forName("android.widget.TextView");
來獲取ListenerInfo的實例的。

要記得字段方法屬於哪個類就得在哪個類hook,通過子類或者父類直接獲取是獲取不到的。

hook技術講解完畢,其它長按監聽器以此類推了。。。這裏特別的是adapterView(如listview/gridview)就沒必要hook了,因爲它自身就帶有getxxx,可以直接獲取實例,重新設置監聽器就可以了:代碼如下:
 /**
     * hook到Listview的listener
     *
     * @param viewGroup
     */
    private void hookListViewListener(ViewGroup viewGroup) {//已經設置過的不會重新設置
        if (viewGroup instanceof ListView) {
            ListView listView = (ListView) viewGroup;
            AdapterView.OnItemClickListener itemClickListener = listView.getOnItemClickListener();
            if (null != itemClickListener && !(itemClickListener instanceof OnItemClickListenerProxy)) {
                if (null == listView.getTag(R.id.tag_onItemClick)) {//還沒hook過
                    listView.setOnItemClickListener(new OnItemClickListenerProxy(itemClickListener, proxyListenerConfigBuilder.getOnItemClickProxyListener()));
                    setHookedTag(listView, R.id.tag_onItemClick);
                }
            }
            AdapterView.OnItemLongClickListener itemLongClickListener = listView.getOnItemLongClickListener();
            if (null != itemLongClickListener && !(itemLongClickListener instanceof OnItemLongClickListenerProxy)) {
                if (null == listView.getTag(R.id.tag_onItemLong)) {//還沒hook過
                    listView.setOnItemLongClickListener(new OnItemLongClickListenerProxy(itemLongClickListener, proxyListenerConfigBuilder.getOnItemLongClickProxyListener()));
                    setHookedTag(listView, R.id.tag_onItemLong);
                }
            }
            AdapterView.OnItemSelectedListener itemSelectedListener = listView.getOnItemSelectedListener();
            if (null != itemSelectedListener && !(itemSelectedListener instanceof OnItemSelectedListenerProxy)) {
                if (null == listView.getTag(R.id.tag_onitemSelected)) {//還沒hook過
                    listView.setOnItemSelectedListener(new OnItemSelectedListenerProxy(itemSelectedListener, proxyListenerConfigBuilder.getOnItemSelectedProxyListener()));
                    setHookedTag(listView, R.id.tag_onitemSelected);
                }
            }
        }
    }

細心的你一定會發現裏邊多了好些邏輯,比如setHookTag,比如一系列的if else...等等它們是幹啥用的呢,聰明的你一定猜出來了,對,就是用於優化的。。。。
我們沒必要重新hook都要將已經hook過得重新hook,不必要hook也hook,所以將hook過以及不需要hook的view直接跳過,提升好大的效率。接下來就結合代碼一起講解
private void hookOnClickListener(View view, boolean isScrollAbsListview) {
        if (!view.isClickable()) {//默認是不可點擊的,只有設置監聽器纔會設置爲true,證明沒有設置點擊事件或者初始化時沒有設置點擊事件
            Log.d(TAG, "isClickable name = " + view.getClass().getSimpleName());
            return;
        }
        Log.d(TAG, "null != view.getTag(R.id.tag_onclick) = " + (null != view.getTag(R.id.tag_onclick)));
        if (!isScrollAbsListview && null != view.getTag(R.id.tag_onclick)) {//已經hook過,並且不是滾動的listview,不用再hook了
            return;
        }
//...
}
view大部分默認都是不可點擊的(除了button),直到設置監聽器纔是可點擊狀態所以第一行就可以把那些沒設置監聽器的都給過濾掉

if (!isScrollAbsListview && null != view.getTag(R.id.tag_onclick)) {//已經hook過,並且不是滾動的listview,不用再hook了
            return;
        }

isScrollAbsListview主要是listview或gridView滾動的時候都要重新hook itemview的監聽器。
null != view.getTag(R.id.tag_onclick)這是hook過的view不再重新hook,這就保證了唯一性,也不會剩下很大的性能,畢竟我們是通過遞歸查找所有的子view的,接下來就說說怎麼查找所有的view的
public void hookStart(Activity activity) {
        if (null != activity) {
            View view = activity.getWindow().getDecorView();
            if (null != view) {
                if (view instanceof ViewGroup) {
                    hookStart((ViewGroup) view);
                } else {
                    hookOnClickListener(view, false);
                    hookOnLongClickListener(view, false);
                }
            }
        }
    }
通過activity我們就很容易就拿到了decorView,所以根據decorView就可以遞歸查找所有的子view以及需要hook的
子view
 /**
     * hook掉viewGroup
     *
     * @param viewGroup
     * @param isScrollAbsListview lsitview或gridView是否滾動:true:滾動則重新hook,false:表示view不是listview或者gridview或者沒滾動
     */
    public void hookStart(ViewGroup viewGroup, boolean isScrollAbsListview) {
        if (viewGroup == null) {
            return;
        }
        int count = viewGroup.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = viewGroup.getChildAt(i);
            if (view instanceof ViewGroup) {//遞歸查詢所有子view
                // 若是佈局控件(LinearLayout或RelativeLayout),繼續查詢子View
                hookStart((ViewGroup) view, isScrollAbsListview);
            } else {
                hookOnClickListener(view, isScrollAbsListview);
                hookOnLongClickListener(view, isScrollAbsListview);
            }
        }
        hookOnClickListener(viewGroup, isScrollAbsListview);
        hookOnLongClickListener(viewGroup, isScrollAbsListview);
        hookListViewListener(viewGroup);
    }

通過這個直接就可以hook成功,親測成功。
到這裏就分析完畢了,有看不懂的地方或者又發現錯誤的地方,請不吝賜教。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章