1. 程式人生 > >java 系列(一) 動態代理(下)

java 系列(一) 動態代理(下)

前兩章想必大家都知道動態代理是怎麼回事,本小節的內容是動態代理的實踐操作了。在Android專案中如何實現按鈕的防雙擊(防抖動)。
這個在Android上又叫Hook技術

1.前言

對於一些特定需求使用也是非常無可奈何的,比如Android裡面對所有的點選事件進行一定的操作,比如防雙擊(防抖動),插樁等。
對於這種需求的解決方案肯定不止一個了,現在通用的(大眾的)解決方案有六個:

1、每個呼叫的時候處理,點選第一下之後將按鈕不可點選狀態,輪詢一定時間之後變為可點選狀態(程式碼不貼了,估計沒人會這麼寫)
2、寫一個工具類,返回布林型,在裡面計算點選週期等,(同樣不建議)
3、複寫view.onClickeListener,重新定義一個抽象類,承接OnClickListener的事件,在進行處理完之後分發,

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

    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); } } }

4、RxBinding操作,或者RXJava自己封裝
通過學習RXJava可知有一個操作符throttleFirst ,作用是在一定時間內的時間,只發送第一條事件,和debounce,作用是在一定時間沒有變化才會傳送事件。
所以可以使用RxBinding:

    RxView.clickEvents
(button) .throttleFirst(500, TimeUnit.MILLISECONDS) .subscribe(clickAction);

看起來是不是很簡單,但是要匯入Rxjava相關的框架,還會破壞butterknife的結構,小夥伴可以想想怎麼寫。

5、使用裝飾器模式
理論上可以實現,但沒有寫過,小夥伴可以試試。
6、動態代理
本節內容的重頭戲,在下部分詳細概述怎麼寫的。

2.動態代理實現仿雙擊

  • #確定需求
    我們的具體需求是什麼,android上的動態代理的形式和Java有什麼不同,雖然Android程式是用java編寫的(原生)。
    1. 首次Android中的Activity是有生命週期的,所以要在所有使用的地方註冊
    2. 找到所要插入的點
      每次使用SetOnClickListener的方法,在View的方法裡面


      都會使用ListenerInfo這個類,下面看看這個類

所以我們按圖索驥,一步一步的找到真正實現的介面的地方,就是在ListenerInfo的OnClickListener。對於這個類我們也可以看出,所有的觸控事件(包括滑動,長按,按鍵等)都是在這個位置進行監聽的。
下面我們來寫動態代理的程式碼:

      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);//獲取已設定過的監聽器

            if (isScrollAbsListview && onClickListerObj instanceof OnClickListenerProxy) {//針對adapterView的滾動item複用會導致重複hook代理監聽器
                return;
            }
            //hook事件,設定自定義的載體事件監聽器
            onClickListerField.set(listenerInfoInstance, new OnClickListenerProxy(onClickListerObj, proxyListenerConfigBuilder.getOnClickProxyListener()));
            setHookedTag(view, R.id.tag_onclick);

其中OnClickListenerProxy就是我們要實現的物件,在這裡要注意給view設定一個Tag,否則會出現重複代理的情況。
下面我們來看看這個代理物件的實現(其實很簡單的):

    public class OnClickListenerProxy implements View.OnClickListener {

    private static final String TAG = "OnClickListenerProxy";
    private View.OnClickListener onClickListener;
    private int MIN_CLICK_DELAY_TIME = 1000;
    private long lastClickTime = 0;


    private OnListenerProxyCallBack.OnClickProxyListener onClickProxyListener;

    public OnClickListenerProxy(View.OnClickListener onClickListener, OnListenerProxyCallBack
            .OnClickProxyListener onClickProxyListener) {
        this.onClickListener = onClickListener;
        this.onClickProxyListener = onClickProxyListener;
    }

    @Override
    public void onClick(final View v) {
        long currentTime = Calendar.getInstance().getTimeInMillis();
        //System.out.println("--------------" + (currentTime - lastClickTime) + "--------------");
        if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
            lastClickTime = currentTime;
          //  Log.e("OnClickListenerProxy", "OnClickListenerProxy"+v.getTag());
            Context context = v.getContext();
            if (context instanceof Activity) {
               // Log.e("OnClickListenerProxy", context.getClass().getSimpleName());
            }
            if (null != onClickProxyListener) {//點選代理回撥
                onClickProxyListener.onClickProxy(v);
            }
            if (null != onClickListener) {
                onClickListener.onClick(v);
            }
        }
    }
    }

通過Context可以判斷Activity的,和獲取Activity的具體名稱,對於插樁是方便的。
同理我們可以實現對於長按事件的監聽,甚至於對Listview的Item的點選事件,Recyclerview的Item的點選事件。
下面我們來看看Hook的代理的入口:

     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);
                }
            }
        }
    }

這只是一種很簡單的情況,但如果像列表控制元件帶滾動的形式,又是另一種處理方式,這是因為Android內部的快取機制導致的這樣的問題。

    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);
    }

必須到遞迴獲取到所有的view控制元件才可以繼續向下執行。

對於在基類裡面呼叫代理呢,肯定是要在view繪製完全的時候,

     private boolean isHookListener = false;

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (isHookListener) {//防止退出的時候還hook
            return;
        }

        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {//等待view都執行完畢之後再hook,否則onLayoutChange執行多次就會hook多次
                HookViewManager.getInstance().hookStart((Activity) mContext);
                isHookListener = true;
            }
        });

    }

直到這一步才正式的代理了view的相關事件的監聽。

3.結束語

會思考的童鞋會由此思考Hook技術是怎麼回事?

1.Hook英文翻譯為“鉤子”,而鉤子就是在事件傳送到終點前截獲並監控事件的傳輸,像個鉤子鉤上事件一樣,並且能夠在鉤上事件時,處理一些自己特定的事件;
2.Hook使它能夠將自己的程式碼“融入”被勾住(Hook)的程序中,成為目標程序的一部分;
3.在Andorid沙箱機制下,Hook是我們能通過一個程式改變其他程式某些行為得以實現;

第一條是不是很熟悉,其實在java層面大部分的Hook都是通過代理實現的,但Hook技術不止包括java層面,還有Native層面,也就是C/C++層面,Android中著名的Hook框架就是——Xposed平臺。
Hook技術的成功很廣泛,只要你像在Android手機上做點黑科技,Hook技術是你必不可少的知識點,包括現在著名的外掛化浪潮,也是在其基礎上引申拓展的。

動態代理三部分講完了,下節將開始我們新的學習。