Android防抖动方案总结

首先讨论防抖动的必要性

一段不防抖动的代码示例:

@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、让服务端处理,我们一般会做防抖动——即将点击事件过滤调。

由此我们可以看到防抖动的必要性:
如果不防抖动,则会触发许多无用的网络请求、异步任务,浪费系统资源、后台进程资源。
如果做了抖动,我们将收获流畅的UI体现,系统稳定性更高,何乐不为?

防抖动方案按事件的产生顺序和处理顺序的方向来划分,可分为“先到达先处理型”和“后到达后处理型”
本文包括以下几种

  1. 传统计算时间间隔
  2. RxJava
  3. RxBinding
  4. 同一任务执行最后一次
    前三个是先到达先处理型,第四个是后到的后处理型

传统计算时间间隔

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

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

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

@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返回给上层,进行下一步的业务处理
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);
    }
}

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

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回调

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
集成方式:

 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任务。

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

使用起来也很简单:
将文章前面提到的错误代码改为:

@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才会被执行。

总结

posted on 2024-06-01 11:57  杨超凡随笔的地方  阅读(35)  评论(0编辑  收藏  举报