Tmp detached view should be removed from RecyclerView before it can be recycled

Tmp detached view should be removed from RecyclerView before it can be recycled

问题场景

在开发一个使用 RecyclerView 的应用时,页面中使用了 RecyclerView.ItemDecoration 来实现动画效果。然而,在频繁切换页面时,应用崩溃并抛出了以下异常:

java 复制代码
java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView before it can be recycled: ViewHolder{123456789, position=5, id=-1, oldPos=-1, pLpos:-1 scrap tmpDetached no parent}
    at androidx.recyclerview.widget.RecyclerView.recycleViewHolderInternal(RecyclerView.java:6666)
    at androidx.recyclerview.widget.RecyclerView.removeDetachedViewInternal(RecyclerView.java:6620)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4212)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3909)
    at androidx.recyclerview.widget.RecyclerView.consumePendingUpdateOperations(RecyclerView.java:1898)
    at androidx.recyclerview.widget.RecyclerView$1.run(RecyclerView.java:409)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:947)
    at android.view.Choreographer.doCallbacks(Choreographer.java:761)
    at android.view.Choreographer.doFrame(Choreographer.java:693)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:193)
    at android.app.ActivityThread.main(ActivityThread.java:6669)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

源码分析

通过分析 RecyclerView 的源码,我们发现问题的根源在于 recycleViewHolderInternal 方法。该方法在回收 ViewHolder 时,会检查 ViewHolder 的状态。如果 ViewHolder 处于临时分离(tmp detached)状态,则会抛出上述异常。

java 复制代码
/**
 * 回收 ViewHolder 对象,确保其可以被重新利用。
 * 检查 ViewHolder 的状态,并根据不同的条件将其缓存或放入回收池中。
 *
 * @param holder 需要回收的 ViewHolder 对象
 */
void recycleViewHolderInternal(ViewHolder holder) {
    // 检查 ViewHolder 是否为 scrap 状态或是否已附加到父视图
    if (!holder.isScrap() && holder.itemView.getParent() == null) {
        // 检查 ViewHolder 是否是临时分离(tmp detached)状态
        if (holder.isTmpDetached()) {
            throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel());
        } else if (holder.shouldIgnore()) {
            // 检查 ViewHolder 是否被标记为忽略(ignored)
            throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel());
        } else {
            // 检查瞬态状态是否阻止回收
            boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
            // 如果阻止且适配器存在,则尝试通过适配器的 onFailedToRecycleView 方法强制回收
            boolean forceRecycle = RecyclerView.this.mAdapter != null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder);
            boolean cached = false;
            boolean recycled = false;

            // 如果允许回收(forceRecycle 或 isRecyclable 为 true)
            if (forceRecycle || holder.isRecyclable()) {
                // 尝试将 ViewHolder 缓存到 mCachedViews 中
                if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) {
                    int cachedViewSize = this.mCachedViews.size();
                    // 如果缓存已满则移除最早缓存的视图
                    if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) {
                        this.recycleCachedViewAt(0);
                        --cachedViewSize;
                    }

                    int targetCacheIndex = cachedViewSize;
                    // 根据预取策略调整缓存位置
                    if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        int cacheIndex;
                        for (cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) {
                            int cachedPos = ((ViewHolder) this.mCachedViews.get(cacheIndex)).mPosition;
                            if (!RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }

                    // 将 ViewHolder 添加到缓存中
                    this.mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }

                // 如果未能成功缓存,则将其添加到回收池中
                if (!cached) {
                    this.addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            }

            // 从 mViewInfoStore 中移除 ViewHolder
            RecyclerView.this.mViewInfoStore.removeViewHolder(holder);
            // 如果既未缓存也未回收且瞬态状态阻止回收,则清除 ViewHolder 的所有者引用
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }
    } else {
        // 抛出异常,当 ViewHolder 处于不允许回收的状态时(如 scrap 状态、临时分离、忽略状态等)
        throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + RecyclerView.this.exceptionLabel());
    }
}

常见场景

RecyclerView 中,ViewRecyclerView 分离的场景通常与 RecyclerView 的内部机制和生命周期有关。以下是一些常见的场景:

  1. 视图被回收RecyclerView 会回收不再可见的 View,以便重用它们来显示新的数据项。
  2. 视图被移除RecyclerView 中的数据项被删除,导致对应的 View 被移除。
  3. 视图被重新绑定RecyclerViewView 从旧的数据项解绑,准备绑定到新的数据项。
  4. 布局或适配器发生变化RecyclerView 的布局管理器或适配器发生变化,导致 View 被分离。
  5. 视图被手动分离 :开发者手动调用 RecyclerView.removeView()RecyclerView.removeViewAt() 方法移除 View
  6. RecyclerView 被销毁RecyclerView 所在的 ActivityFragment 被销毁,导致所有 View 被分离。
  7. 视图被临时分离RecyclerView 在布局过程中临时分离 View,以便重新计算布局。

本次问题场景

在应用内部频繁切换 Activity 时,RecyclerView 被销毁和重新绑定。具体地,在 RecyclerView.ItemDecorationgetItemOffsets 方法中,使用了 getChildAdapterPosition 却没有判断 position 的有效性。在进行测量、布局、绘制 RecyclerView 动画效果时,导致了上述异常。

java 复制代码
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    int position = parent.getChildAdapterPosition(view);
    // 直接处理业务逻辑
    ...
}

getChildAdapterPosition 的返回值可能为 NO_POSITION(即 -1),如果 view 已经与 RecyclerView 分离,getChildAdapterPosition 会返回 NO_POSITION

修改建议

  1. 不使用动画 :通过 setItemAnimator(null) 禁用 RecyclerView 的动画效果,这样 RecyclerView 源码里面不会走到 mItemAnimator.runPendingAnimations() 运行动画。

    java 复制代码
    recyclerView.setItemAnimator(null);
  2. 检查 position 的有效性 :在 getItemOffsets 方法中,检查 position 是否有效,无效则不处理后面的逻辑。

    java 复制代码
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view);
        // 检查 position 是否有效,无效就不处理后面的逻辑
        if (position == RecyclerView.NO_POSITION) {
            Log.d("ItemDecoration", "View is detached, position is NO_POSITION");
            return;
        }
        ...
    }

总结

通过分析 RecyclerView 的源码和常见场景,我们了解到 ViewHolder 在回收时可能会处于临时分离状态,从而导致异常。为了避免此类问题,我们可以在 ItemDecoration 中检查 position 的有效性,或者禁用 RecyclerView 的动画效果。这些修改可以有效避免应用崩溃,提升用户体验。

相关推荐
火柴就是我11 小时前
mmkv的 mmap 的理解
android
没有了遇见11 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong11 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强12 小时前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸12 小时前
【底层机制】 Android ION内存分配器深度解析
android·面试
你听得到1112 小时前
肝了半个月,我用 Flutter 写了个功能强大的图片编辑器,告别image_cropper
android·前端·flutter
KevinWang_12 小时前
Android 原生 app 和 WebView 如何交互?
android
用户693717500138412 小时前
Android Studio中Gradle、AGP、Java 版本关系:不再被构建折磨!
android·android studio
杨筱毅13 小时前
【底层机制】Android低内存管理机制深度解析
android·底层机制
二流小码农14 小时前
鸿蒙开发:this的指向问题
android·ios·harmonyos