Android插件化系列第(三)篇---Hook技術(shù)之View點(diǎn)擊劫持

版權(quán)聲明:本文為LooperJing原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明出處!

昨天有好幾個(gè)小伙伴簡(jiǎn)信問(wèn)我,View.onClick怎么hook?回想前幾個(gè)月前,公司的項(xiàng)目在百度手機(jī)助手上線,在快速點(diǎn)擊的時(shí)候會(huì)跳轉(zhuǎn)兩次Activity或者兩個(gè)Dialog等等,為了能夠順利通過(guò)百度的測(cè)試,老大叫我將所有onClick全部要優(yōu)化處理,避免用戶快速多次點(diǎn)擊,于是乎,我寫了下面的代碼

public abstract class NoDoubleClickListener implements View.OnClickListener {
    
    private int MIN_CLICK_DELAY_TIME = 1000;

    private long lastClickTime = 0;

    public abstract void onNoDoubleClick(View v);

    @Override
    public void onClick(View v) {
        long currentTime = Calendar.getInstance().getTimeInMillis();

        if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
            lastClickTime = currentTime;
            onNoDoubleClick(v);
        }
    }
}

然后我打算這樣來(lái)弄

  btn.setOnClickListener(new NoDoubleClickListener() {
            @Override
            public void onNoDoubleClick(View v) {
               //something
            }
 });

可是面臨一個(gè)問(wèn)題,有這么多,改到何年馬月???

OK,這個(gè)是我以前碰到的一個(gè)好蛋疼的問(wèn)題,再比如,在不侵入業(yè)務(wù)代碼的情況下監(jiān)聽所有的點(diǎn)擊事件并記錄所有的點(diǎn)擊數(shù),用于統(tǒng)計(jì)熱點(diǎn)頁(yè)面和其他一些分析工作,你怎么辦呢?現(xiàn)在介紹一個(gè)如何Hook掉View的onClick方法,相對(duì)與上一篇,這個(gè)很簡(jiǎn)單了。

1、第一步尋找Hook點(diǎn):

去看setOnClickListener里面做了什么?

 btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {        
          }
   });
  /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
 ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

看完了上面,就能猜到我們?cè)O(shè)置的Listener最終是被賦值給ListenerInfo的mOnClickListener成員了,ListenerInfo的實(shí)例可以說(shuō)是信息的載體,那么很簡(jiǎn)單,只要把mOnClickListener替換掉,在ListenerInfo中還有mOnLongClickListener,mOnFocusChangeListener兩個(gè)成員,分別對(duì)應(yīng)了長(zhǎng)按事件與焦點(diǎn)變化事件,所以處理長(zhǎng)按事件與焦點(diǎn)變化事件與此類似。

public class HookViewClickUtil {

    public static HookViewClickUtil getInstance() {
        return UtilHolder.mHookViewClickUtil;
    }

    private static class UtilHolder {
        private static HookViewClickUtil mHookViewClickUtil = new HookViewClickUtil();
    }

    public static void hookView(View view) {
        try {
            Class viewClazz = Class.forName("android.view.View");
            //事件監(jiān)聽器都是這個(gè)實(shí)例保存的
            Method listenerInfoMethod = viewClazz.getDeclaredMethod("getListenerInfo");
            if (!listenerInfoMethod.isAccessible()) {
                listenerInfoMethod.setAccessible(true);
            }
            Object listenerInfoObj = listenerInfoMethod.invoke(view);

            Class listenerInfoClazz = Class.forName("android.view.View$ListenerInfo");

            Field onClickListenerField = listenerInfoClazz.getDeclaredField("mOnClickListener");

            if (!onClickListenerField.isAccessible()) {
                onClickListenerField.setAccessible(true);
            }
            View.OnClickListener mOnClickListener = (View.OnClickListener) onClickListenerField.get(listenerInfoObj);
            //自定義代理事件監(jiān)聽器
            View.OnClickListener onClickListenerProxy = new OnClickListenerProxy(mOnClickListener);
            //更換
            onClickListenerField.set(listenerInfoObj, onClickListenerProxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    //自定義的代理事件監(jiān)聽器
    private static class OnClickListenerProxy implements View.OnClickListener {

        private View.OnClickListener object;

        private int MIN_CLICK_DELAY_TIME = 1000;

        private long lastClickTime = 0;

        private OnClickListenerProxy(View.OnClickListener object) {
            this.object = object;
        }

        @Override
        public void onClick(View v) {
            //點(diǎn)擊時(shí)間控制
            long currentTime = Calendar.getInstance().getTimeInMillis();
            if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
                lastClickTime = currentTime;
                Log.e("OnClickListenerProxy", "OnClickListenerProxy");
                if (object != null) object.onClick(v);
            }
        }
    }
}

使用起來(lái)也是非常簡(jiǎn)單,首先在MainActivity的View渲染完畢的時(shí)候進(jìn)行注入,即在 getWindow().getDecorView().post()中。

public class MainActivity extends Activity {

    private Button  btn;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final View btn = findViewById(R.id.btn);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("MainActivity","Button 被點(diǎn)擊了");
            }
        });

        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                HookViewClickUtil.hookView(btn);
            }
        });
    }
}

執(zhí)行結(jié)果:

OK,到此完成了,至于怎么獲取頁(yè)面的所有View,調(diào)用 HookViewClickUtil.hookView(view),就不多說(shuō)了。

Please accept mybest wishes for your happiness and success !

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

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

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