RecyclerView之Scrapped or attached views may not be recycled. isAttached=true

290 阅读3分钟

这个错误是在 RecyclerView 的Recycler 类中抛出的,具体位置在recycleViewHolderInternal(ViewHolder holder)方法里。当尝试回收一个已附加到父视图(isAttached=true)的 ViewHolder 时,就会触发这个异常。

错误根源

RecyclerView 的回收机制要求:

  1. 只有未附加到 RecyclerView 的 ViewHolder 才能被回收
  2. 临时废弃(scrap)状态的 ViewHolder 需先从 scrap 状态移除才能回收
RecyclerView中内部类RecyclerrecycleViewHolderInternal()方法对应代码段如下:
if (holder.isScrap() || holder.itemView.getParent() != null) {
    throw new IllegalArgumentException(
            "Scrapped or attached views may not be recycled. isScrap:"
                    + holder.isScrap() + " isAttached:"
                    + (holder.itemView.getParent() != null) + exceptionLabel());
}

常见触发场景

  1. 异步操作:在 ViewHolder 绑定数据后,异步操作导致视图状态变化,此时 RecyclerView 尝试回收它
  2. 自定义动画:动画执行期间,ViewHolder 仍被视为附加状态
  3. 错误的 notify 调用:如在动画或布局过程中调用notifyDataSetChanged()

解决方向

  1. 确保在 RecyclerView 执行动画或布局时,不进行数据更新操作

  2. 使用adapter.notifyItemRangeChanged()代替notifyDataSetChanged()

  3. 在自定义动画结束后再回收 ViewHolder

  4. 避免在 ViewHolder 的回调方法中执行耗时操作

这个错误本质是 RecyclerView 的回收策略与视图状态管理之间的冲突,需要在代码中确保视图状态与回收操作的同步性。

这个异常发生于尝试回收仍被附加到父视图的 ViewHolder 时。下面为你提供几种解决办法:

1. 确认数据更新操作的时机

要保证在 RecyclerView 处于稳定状态时才进行数据更新。可以借助post方法来延迟执行可能会引发异常的操作。

java

recyclerView.post(new Runnable() {
    @Override
    public void run() {
        // 执行notify相关操作
        adapter.notifyItemRemoved(position);
    }
});

2. 防止在动画进行中回收 ViewHolder

在执行自定义动画时,要避免调用notify系列方法。可以在动画结束的回调里进行数据更新。

java

// 自定义ItemAnimator
public class CustomItemAnimator extends DefaultItemAnimator {
    @Override
    public boolean animateRemove(final ViewHolder holder) {
        // 自定义移除动画
        ViewCompat.animate(holder.itemView)
                .alpha(0)
                .setDuration(getRemoveDuration())
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // 动画结束后重置视图状态
                        resetAnimation(holder);
                        // 通知动画结束
                        dispatchRemoveFinished(holder);
                        // 关键步骤:标记动画已完成
                        holder.itemView.setAlpha(1);
                    }
                })
                .start();
        return true;
    }
}

3. 优化 ViewHolder 的回收逻辑

在需要手动回收 ViewHolder 的情况下,先将其从父视图中分离。

java

// 在adapter中
public void safelyRecycleViewHolder(ViewHolder holder) {
    if (holder.itemView.getParent() != null) {
        ((RecyclerView) holder.itemView.getParent()).removeView(holder.itemView);
    }
    recyclerView.getRecycler().recycleViewHolderInternal(holder);
}

4. 处理异步操作

如果在 ViewHolder 中有异步操作,要确保在视图被回收前终止这些操作。

java

@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
    super.onViewRecycled(holder);
    // 取消异步任务
    holder.cancelAsyncTasks();
}

5. 使用适当的 notify 方法

优先使用更精确的notifyItemChanged(),而非notifyDataSetChanged()

java

// 不要用这个
adapter.notifyDataSetChanged();

// 要用这个
adapter.notifyItemChanged(position);

6. 检查自定义 LayoutManager

若你使用的是自定义 LayoutManager,要确保在onDetachedFromWindow()方法中正确处理视图回收。

java

@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
    super.onDetachedFromWindow(view, recycler);
    // 释放所有缓存的视图
    recycler.clear();
}

7. 避免在 onBindViewHolder 中执行耗时操作

onBindViewHolder里执行耗时操作可能会使 ViewHolder 长时间处于附加状态。

java

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    // 不要在这里执行网络请求等耗时操作
    // 应该在ViewModel中处理数据,然后通过LiveData传递到这里
}

总结

这个异常主要是由于视图状态管理和 RecyclerView 回收机制之间存在冲突。解决的关键在于:

  1. 保证视图在被回收前处于未附加状态

  2. 防止在动画或异步操作期间回收视图(这就是我这次遇到的问题,没有合理处理ITEM中的动画导致)

  3. 合理运用 RecyclerView 的回调方法来管理视图状态

如果你的项目使用了 DataBinding 或者 ViewBinding,要确保它们的生命周期与 ViewHolder 的状态保持同步。