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 中,View 与 RecyclerView 分离的场景通常与 RecyclerView 的内部机制和生命周期有关。以下是一些常见的场景:

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

本次问题场景

在应用内部频繁切换 Activity 时,RecyclerView 被销毁和重新绑定。具体地,在 RecyclerView.ItemDecoration 的 getItemOffsets 方法中,使用了 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 的动画效果。这些修改可以有效避免应用崩溃,提升用户体验。

相关推荐
三少爷的鞋4 小时前
Android 架构七条铁律:架构师真正设计的不是代码,而是秩序
android
xiangxiongfly9156 小时前
Android GsonFactory总结
android·gson·gsonfactory
WythePlus8 小时前
Android 14 SELinux策略深度解析
android·安全策略·selinux策略
俩个逗号。。11 小时前
Kotlin flow 详解
android·开发语言·kotlin
橙光成长13 小时前
android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送
android·1024程序员节
深圳雨林凯AI13 小时前
图案裂变的底色衰减:alpha通道、亮度对比与白墨分色的量化拆解
android
mmsx13 小时前
Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存
android
恋猫de小郭16 小时前
Flutter 可以更直接地调用原生 API 了,Dart Direct Native Interop
android·前端·flutter
2501_9160088916 小时前
iPhone 设备信息查看,用 KeyMob 看型号、序列号、电池健康和 UDID
android·ios·小程序·https·uni-app·iphone·webview
好的佩奇1 天前
蓝牙之通信架构
android·flutter·蓝牙