You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

异步操作下RecyclerView不显示数据并抛出ConcurrentModificationException

分析RecyclerView适配器异步更新时的ConcurrentModificationException及数据显示问题

首先看你遇到的两个核心问题:ConcurrentModificationException异常和异步更新后RecyclerView为空,我们逐个拆解原因和解决办法:

一、ConcurrentModificationException的直接原因

你在AsyncTaskRunner.doInBackground()里犯了一个典型的ArrayList迭代错误:

Iterator<ProfileSearchResult> iter = profileList.iterator();
while (iter.hasNext()) {
    ProfileSearchResult result = iter.next();
    if(false == profileListNew.contains(result)){
        profileList.remove(result); // 这里直接调用集合的remove方法!
    }
}

ArrayList的迭代器是**快速失败(fail-fast)**的:当迭代器正在遍历集合时,如果集合本身被调用add/remove等结构性修改方法,迭代器会立刻抛出ConcurrentModificationException。正确的做法是使用迭代器自身的remove()方法来删除元素:

iter.remove(); // 替换掉profileList.remove(result)

二、RecyclerView为空的深层原因

即使修复了异常,你的异步更新逻辑还有几个逻辑漏洞,会导致数据没有正确更新到profileList中:

  1. equals方法未重写:profileListNew.contains(result)依赖ProfileSearchResult的equals()方法判断对象是否相同。如果你的ProfileSearchResult类没有重写equals()和hashCode(),默认会比较对象引用,导致原本应该匹配的对象被判定为不相等,最终profileList的增删逻辑完全失效,列表始终为空。
  2. AsyncTask参数与成员变量线程安全问题:你定义的AsyncTask<List<ProfileSearchResult>, Void, Void>没有传入参数(taskRunner.execute()是空参),但在doInBackground里直接操作成员变量profileList和profileListNew——如果主线程在异步任务执行过程中修改了这些成员变量,会导致数据不一致。
  3. 列表更新逻辑低效且易出错:手动遍历两个列表对比增删的方式很容易遗漏边界情况,比如修改了某个元素的属性但对象引用没变时,你的逻辑无法识别这种更新。

三、推荐的修复方案

方案1:修复现有AsyncTask逻辑(快速临时修复)

  • 用迭代器的remove()替代集合的remove()
  • 给ProfileSearchResult重写equals()和hashCode()(比如根据用户ID等唯一标识来实现)
  • 确保profileListNew是每次更新的完整数据集,或者在异步任务开始前创建副本,避免主线程修改:
public void notifyChanges(List<ProfileSearchResult> changes){
    // 创建副本,避免主线程后续修改影响异步任务
    profileListNew = new ArrayList<>(changes);
    new AsyncTaskRunner().execute();
}

// 修正doInBackground的迭代逻辑
@Override
protected Void doInBackground(Void... voids) {
    // 先移除原列表中不在新列表的项
    Iterator<ProfileSearchResult> iter = profileList.iterator();
    while (iter.hasNext()) {
        ProfileSearchResult result = iter.next();
        if(!profileListNew.contains(result)){
            iter.remove(); // 使用迭代器的remove方法
        }
    }
    // 添加新列表中不在原列表的项
    for(ProfileSearchResult result : profileListNew){
        if(!profileList.contains(result)){
            profileList.add(result);
        }
    }
    return null;
}

方案2:使用DiffUtil(官方推荐,更高效可靠)

手动处理列表对比容易出错,Android官方推荐用DiffUtil来计算新旧列表的差异,它可以在后台线程计算,然后在主线程更新RecyclerView,既避免线程安全问题,又能实现局部刷新(比notifyDataSetChanged()更高效):

  1. 创建DiffUtil.Callback类:
public class ProfileDiffCallback extends DiffUtil.Callback {
    private List<ProfileSearchResult> oldList;
    private List<ProfileSearchResult> newList;

    public ProfileDiffCallback(List<ProfileSearchResult> oldList, List<ProfileSearchResult> newList) {
        this.oldList = oldList;
        this.newList = newList;
    }

    @Override
    public int getOldListSize() {
        return oldList.size();
    }

    @Override
    public int getNewListSize() {
        return newList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        // 根据唯一标识判断是否是同一个item(比如用户ID)
        return oldList.get(oldItemPosition).getId().equals(newList.get(newItemPosition).getId());
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        // 判断item内容是否相同,这里要比较所有需要显示的字段
        ProfileSearchResult oldItem = oldList.get(oldItemPosition);
        ProfileSearchResult newItem = newList.get(newItemPosition);
        return oldItem.getName().equals(newItem.getName())
                && oldItem.getTagline().equals(newItem.getTagline())
                && oldItem.getProfilePictureUrl().equals(newItem.getProfilePictureUrl());
    }
}
  1. 修改适配器的notifyChanges方法:
public void notifyChanges(List<ProfileSearchResult> newList){
    // 在后台线程计算差异
    new AsyncTask<Void, Void, DiffUtil.DiffResult>() {
        @Override
        protected DiffUtil.DiffResult doInBackground(Void... voids) {
            return DiffUtil.calculateDiff(new ProfileDiffCallback(profileList, newList));
        }

        @Override
        protected void onPostExecute(DiffUtil.DiffResult diffResult) {
            // 更新原列表,然后通知适配器刷新
            profileList.clear();
            profileList.addAll(newList);
            diffResult.dispatchUpdatesTo(ProfileSearchRecyclerAdapter.this);
        }
    }.execute();
}

这样既避免了ConcurrentModificationException,又能正确更新数据,还能实现局部刷新提升性能。

额外注意点

  • 永远不要在非主线程调用RecyclerView的notify系列方法,notifyDataSetChanged()必须在主线程执行(你的onPostExecute是主线程,这部分是对的)。
  • 尽量避免使用AsyncTask,Android现在推荐用Coroutines或者Executor来处理后台任务,AsyncTask已经被标记为废弃。

内容的提问来源于stack exchange,提问作者Sachin Titus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:25:40