Android/Java事件过滤方案小结

什么是防抖动?举一个典型场景:

笔者处理一个bug,【留言板】留言列表点击编辑,多次点击删除键,提示“留言发送失败” ,问题表现为点击删除按钮多次,发出了同一个请求多次,服务端无法响应返回了失败的响应码。

1
2
3
4
5
6
7
8
9
10
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
MessageBean bean = mbsAdapter.getData().get(position);
if (view.getId() == R.id.iv_delete) {
mbRequest.deleteSingle(bean, position);
StatisticsUtilFamilyMeals.mbDelete(this, MBHomePageActivity.class.getSimpleName());
} else if (view.getId() == R.id.iv_resend) {
mbRequest.updateCacheMessage(bean, position);
}
}

测试人员频繁点击一个Delete按钮,多次发出了同一份Request,该Request中携带了相同的请求体,服务端只能处理一份,先到达的Request将被服务端处理,后到达的Request将被服务端标记为处理失败,客户端会依次收到处理成功、处理失败、处理失败……

以上就是一个典型的点击事件频繁创建的问题,为了防止事件的频繁创建、响应、发出Request、让服务端处理,我们一般会做防抖动——即将点击事件过滤调。

防抖动方案按事件的产生顺序和处理顺序的方向来划分,可分为“先到达先处理型”和“后到达后处理型”

本文包括以下几种

  1. 传统计算时间间隔
  2. RxJava
  3. RxBinding
  4. 同一任务执行最后一次

前三个是先到达先处理型,第四个是厚道的后处理型

传统计算时间间隔#

第一次触发事件的时候记录一个时间戳,下一次触发事件时,再记录一个时间戳,在指定的时间间隔内,返回false,不去执行后续的业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MBUtils {
/**
* 两次点击按钮接口之间的点击间隔不能少于1300毫秒,人为控制这个开关
*/
private static final long MIN_CLICK_DELAY_TIME = 1300;
private static long lastClickTime;

/**
* @return true:频繁点击 ;fasle 不是频繁点击
*/
public boolean isFastClick() {
boolean flag = true;
long curClickTime = System.currentTimeMillis();
if ((curClickTime - lastClickTime) >= MIN_CLICK_DELAY_TIME) {
flag = false;
}
lastClickTime = curClickTime;
return flag;
}
}

将文章开头提到的错误代码改为:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
MessageBean bean = mbsAdapter.getData().get(position);
if (view.getId() == R.id.iv_delete) {
if(MBUtils.isFastClick()){
// 点击太快不予执行
return;
}
mbRequest.deleteSingle(bean, position);
}
}

RxJava#

这种方案的效果是,当一个动作连续触发,则只执行第一次。主要利用了Rxjava#throttleFirst方法的特性,在指定时间间隔内,Observer不会收到subscribe发来的消息,达到防抖动的效果

  • ObservableOnSubscribe用于观察View的Click事件,在接收到系统发来的click消息后,通过onNext传递给它的观察者
  • Observer用于接收消息,一旦Rxjava底层发来了消息,将在onNext处理它,并通过myClickListener返回给上层,进行下一步的业务处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MBUtils {

public interface ThrottleClickListener {
void onClick(View view);
}

public void throttleFirstProcess(long delay, TimeUnit unit, final View view, final ThrottleClickListener myClickListener) {
ObservableOnSubscribe<View> subscribe = new ObservableOnSubscribe<View>() {
@Override
public void subscribe(final ObservableEmitter<View> emitter) throws Exception {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
emitter.onNext(view);
}
});
}
};
Observer<View> observer = new Observer<View>() {
@Override
public void onSubscribe(Disposable d) {
}

@Override
public void onNext(View view) {
myClickListener.onClick(view);
}

@Override
public void onError(Throwable e) {
}

@Override
public void onComplete() {
}
};
Observable
.create(subscribe)
.throttleFirst(delay,unit)
.subscribe(observer);
}

}

将文章开头提到的错误代码改为:

1
2
3
4
5
6
MBUtils.throttleFirstProcess(1, TimeUnit.SECONDS, childView, new MBUtils.ThrottleClickListener() {
@Override
public void onClick(View view) {

}
});

RxBinding#

RxBinding是一款非常强大组件库,用于将特定的UI组件与事件绑定起来,如将一个Button与Button的点击事件绑定。

以下是针对本文提到到频繁点击删除按钮的过滤用法,通过RxView对象,将Button对象与click事件绑定,用户点击Button后,会执行subscribe的Action#Call回调

1
2
3
4
5
6
7
8
9
Button btn = helper.itemView.findViewById(R.id.iv_delete);
RxView.clicks(btn)
.throttleFirst(3, TimeUnit.SECONDS)
.subscribe(new Action1<Void>() {
@Override
public void call(Void aVoid) {

}
});

RxView支持哪些事件?

clicks
longClicks
draws
drag
layoutChange
scrollChange
setVisibility
setClickable
attaches
detaches
focusChanges
globalLayouts
hovers
touches

集成方式:

1
implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0'  # 按需使用 implementation 'com.jakewharton.rxbinding4:rxbinding-core:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-appcompat:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-drawerlayout:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-leanback:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-recyclerview:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-slidingpanelayout:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-swiperefreshlayout:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-viewpager:4.0.0' implementation 'com.jakewharton.rxbinding4:rxbinding-viewpager2:4.0.0' # 按需使用 implementation 'com.jakewharton.rxbinding4:rxbinding-material:4.0.0'

同一任务执行最后一次#

这种方案简单的说,当一个动作连续触发,则只执行最后一次。

查看代码,只有一个方法throttleFirstProcess方法会接收Runnnable对象作为value,以任务名作为key存储在一个ConcurrentHashMap里,每个Runnable的特点是将被延时1秒执行

为了实现只执行最后一次的效果,我们主要利用了ConcurrentHashMap对象put方法的特性,

举个例子,第一次执行Delete任务,ConcurrentHashMap会记下一个key为Delete,value为Rannable的任务;

指定时间间隔内,若第二次触发Delete任务,则ConcurrentHashMap会进行查找,一旦发现存在名为Delete的任务,我们将取出,并cancel掉第一次存入的Delete任务,这样保证了第一笔Delete任务在被执行之前取消掉。最终的效果就是只执行了第二次存入的Delete任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MBUtils {
private static final ScheduledExecutorService SCHEDULE = Executors.newSingleThreadScheduledExecutor();
private static final ConcurrentHashMap<Object, Future<?>> DELAYED_MAP = new ConcurrentHashMap<>();

/**
* 防抖動,只处理最后一次任务
* @param key 任务key值,同key代表过滤
* @param runnable 子线程执行任务
* @param delay 同一任务多少米内过滤
* @param unit 时间单位
*/
public static void throttleFirstProcess(final Object key, final Runnable runnable, long delay, TimeUnit unit) {
final Future<?> prev = DELAYED_MAP.put(key, SCHEDULE.schedule(() -> {
try {
runnable.run();
} finally {
DELAYED_MAP.remove(key);
}
}, delay, unit));
if (prev != null) {
prev.cancel(true);
}
}
}

使用起来也很简单:

将文章前面提到的错误代码改为:

1
2
3
4
5
6
7
8
9
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
MessageBean bean = mbsAdapter.getData().get(position);
if (view.getId() == R.id.iv_delete) {
// 3秒内频繁点击,只处理最后一笔提交的任务
MBUtils.throttleFirstProcess("删除留言",()->{mbRequest.deleteSingle(bean, position); },1, TimeUnit.SECONDS);
...
}
}

1秒内点击多次,会创建n个同名为删除留言的Runnable,只有最后一个存入的Runnable才会被执行。

总结#

优点 缺点 适用场景
传统计算时间间隔 - - -
RxJava 事件异常捕获 需要创建观察者和被观察者 -
RxBinding 事件异常捕获支持大部分系统UI组件 需要及时释放观察者,否则有内存泄漏 -
同一任务执行最后一次 - 任务完成的时间会被延后 对于任务完成时效性不敏感的:删除操作,如删除同一笔留言、同一笔订单、创建虚拟用户创建操作,如添加同一个食材编辑在线文档,只在3秒内无新的编辑时,自动上传文档内容
点击查看
-------------------本文结束 感谢您的阅读-------------------