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編寫的(原生)。
- 首次Android中的Activity是有生命週期的,所以要在所有使用的地方註冊
- 找到所要插入的點
每次使用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技術是你必不可少的知識點,包括現在著名的外掛化浪潮,也是在其基礎上引申拓展的。
動態代理三部分講完了,下節將開始我們新的學習。