java 系列(一) 动态代理(下)

来源:互联网 发布:sql offset fetch 编辑:程序博客网 时间:2024/06/06 10:05

前两章想必大家都知道动态代理是怎么回事,本小节的内容是动态代理的实践操作了。在Android项目中如何实现按钮的防双击(防抖动)。
这个在Android上又叫Hook技术


java 系列: 动态代理(上)-一个简单的动态代理
java 系列: 动态代理(中)-与装饰器模式的搭配与使用
java 系列: 动态代理(下)-与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技术是你必不可少的知识点,包括现在著名的插件化浪潮,也是在其基础上引申拓展的。

动态代理三部分讲完了,下节将开始我们新的学习。