RecyclerView.Adapter异步筛选期间数据更新冲突如何规避?
问题根因
你的代码存在3个核心的多线程安全问题,直接导致了异常和卡顿:
- 你声明
elevators时加了final修饰,却在update和publishResults方法中修改它的引用,语法本身存在冲突,同时该变量会被UI线程渲染逻辑、数据更新逻辑、筛选结果发布逻辑同时访问,没有同步保护,容易触发数组越界异常。 Filter的performFiltering方法运行在独立后台线程,你直接读取dataStore的全局数据集,而30秒一次的更新逻辑同时在修改dataStore的数据集,两个线程同时读写同一个集合会触发ConcurrentModificationException。- 旧筛选任务未完成时如果触发了数据更新,后续旧筛选结果发布到UI会覆盖新数据,导致展示错乱,你之前的等待筛选完成再更新的方案相当于把后台线程的耗时转嫁给了UI线程,自然会卡顿。
修复方案
你可以按照以下步骤修改代码,不需要加锁也不会卡顿:
- 移除
elevators的final修饰,所有对elevators的修改操作都放在UI线程执行,避免多线程同时修改。 - 要求
dataStore.getElevatorList()返回当前数据集的副本,而不是原始集合的引用,从根源上避免多线程同时读写同一个集合。 - 每次触发数据更新时,先丢弃正在执行的旧筛选任务,再更新适配器数据,避免旧的无效筛选结果覆盖新数据。
- 筛选逻辑优先使用当前适配器已加载的数据集副本,不需要每次都读全局
dataStore,减少跨线程数据冲突。
public class ElevatorsAdapter extends RecyclerView.Adapter<ElevatorsAdapter.ElevatorViewHolder> implements Filterable { // 移除final修饰,仅在UI线程修改 private ArrayList<Elevator> elevators; // 标记位,用于控制筛选任务是否需要中断 private volatile boolean isFilterInvalid = false; private CharSequence lastFilterConstraint = ""; public ElevatorsAdapter() { // 读取数据集副本 this.elevators = new ArrayList<>(dataStore.getElevatorList()); } @Override public void onBindViewHolder(ElevatorsAdapter.ElevatorViewHolder holder, int position) { Elevator elevator = elevators.get(position); holder.textViewRow.setText(elevator.getDisplay()); } public void update() { // 所有修改elevators的逻辑都在UI线程执行 isFilterInvalid = true; elevators = new ArrayList<>(dataStore.getElevatorList()); notifyDataSetChanged(); // 数据更新后自动触发上次的筛选条件 if (!TextUtils.isEmpty(lastFilterConstraint)) { getFilter().filter(lastFilterConstraint); } } @Override public int getItemCount() { return elevators.size(); } @Override public Filter getFilter() { return filter; } private final Filter filter = new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { lastFilterConstraint = constraint != null ? constraint : ""; isFilterInvalid = false; List<Elevator> filtered = new ArrayList<>(); // 读取当前适配器的数据集副本,不用读全局dataStore List<Elevator> sourceList = new ArrayList<>(elevators); // 中途数据更新就直接丢弃本次筛选结果 if (isFilterInvalid) { FilterResults results = new FilterResults(); results.values = sourceList; return results; } if (constraint == null || constraint.length() == 0) { filtered.addAll(sourceList); } else { String pattern = constraint.toString().toLowerCase().trim(); for (Elevator e : sourceList) { if (isFilterInvalid) break; // 中途失效就终止遍历 if (e.getDisplay().toLowerCase().contains(pattern)) { filtered.add(e); } } } FilterResults results = new FilterResults(); results.values = filtered; return results; } @Override protected void publishResults(CharSequence constraint, @NonNull FilterResults results) { // 筛选任务已经失效就丢弃结果 if (isFilterInvalid) return; elevators = (ArrayList<Elevator>) results.values; notifyDataSetChanged(); } }; }
优化说明
修改后的逻辑不会出现多线程冲突,也不会卡顿:
- 所有集合读取都用副本,不会出现多线程同时读写同一个集合的问题
- 数据更新时只需要修改标记位就可以终止旧的筛选任务,不需要等待,不会阻塞UI线程
- 无效的筛选结果会被直接丢弃,不会覆盖新更新的数据
内容的提问来源于stack exchange,提问作者M.Toy
相关产品推荐
相关产品推荐

