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 的动画效果。这些修改可以有效避免应用崩溃,提升用户体验。

相关推荐
whysqwhw31 分钟前
Android MQTT 使用
android
与火星的孩子对话36 分钟前
Unity大型场景性能优化全攻略:PC与安卓端深度实践 - 场景管理、渲染优化、资源调度 C#
android·unity·性能优化·c#
_小马快跑_6 小时前
Android | Matrix.setPolyToPoly() 图像变换详解
android
tangweiguo030519877 小时前
Flutter 与 Android NDK 集成实战:实现高性能原生功能
android·flutter
_祝你今天愉快9 小时前
Android SurfaceView & TextureView
android·性能优化
q5507071779 小时前
uniapp/uniappx实现图片或视频文件选择时同步告知权限申请目的解决华为等应用市场上架审核问题
android·图像处理·uni-app·uniapp·unix
李新_10 小时前
一个复杂Android工程开发前我们要考虑哪些事情?
android·程序员·架构
casual_clover11 小时前
Android 中解决 Button 按钮背景色设置无效的问题
android·button
峥嵘life12 小时前
Android14 通过AMS 实例获取前台Activity 信息
android·安全