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编写的(原生)。- 首次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技术是你必不可少的知识点,包括现在著名的插件化浪潮,也是在其基础上引申拓展的。
动态代理三部分讲完了,下节将开始我们新的学习。
- java 系列(一) 动态代理(下)
- java 系列(一) 动态代理(中)
- Java动态代理(一)
- java动态代理(下)
- 【AOP系列】(一)—静态代理VS动态代理(Java)
- java动态代理一(java自带动态代理)
- java 系列: 动态代理(上)
- Java代理之静态代理与动态代理(一)
- java动态代理学习(一)
- Java动态代理学习文章(一)
- Java动态代理剖析(一)
- java设计模式(一):动态代理
- java基础知识(一) JDK动态代理
- 动态代理(一)
- 动态代理(一)---JDK动态代理
- java基础与提高系列-java 代理(静态代理和动态代理)
- 动态代理(java)
- Java深入浅出系列(四)——深入剖析动态代理--从静态代理到动态代理的演化
- ffmpeg入门学习——文档4:创建线程
- 关于 UGUI 字体花屏或乱码。
- python list 遍历删除的正确方法
- QBXT 字符串
- HAWQ + MADlib 玩转数据挖掘之(二)——矩阵
- java 系列(一) 动态代理(下)
- 虚函数表的特点
- 空调选购
- maven的安装
- 16 多校 6
- 数据存储(2):SharedPreferences存储
- static
- C++中 sprintf函数的用法
- 线性可分支持向量机思想与公式推导