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 分钟前
Find Hub迎来重大升级,UWB技术实现厘米级精准定位,离线追踪覆盖更广
android·google findhub
悠哉清闲23 分钟前
SoundPool
android
鹏多多26 分钟前
flutter-使用url_launcher打开链接/应用/短信/邮件和评分跳转等
android·前端·flutter
2501_9159214328 分钟前
iOS 性能分析工具全景解析,构建从底层诊断到真机监控的多层级性能分析体系
android·ios·小程序·https·uni-app·iphone·webview
zhixingheyi_tian30 分钟前
TestDFSIO 之 热点分析
android·java·javascript
2501_9159090632 分钟前
如何防止 IPA 被反编译,从攻防视角构建一套真正有效的 iOS 成品保护体系
android·macos·ios·小程序·uni-app·cocoa·iphone
触想工业平板电脑一体机33 分钟前
【触想智能】工业触控一体机在工业应用中扮演的角色以及其应用场景分析
android·大数据·运维·电脑·智能电视
克喵的水银蛇36 分钟前
Flutter 入门实战:从零搭建跨平台 HelloWorld 应用(适配鸿蒙 / 安卓 /iOS)
android·flutter·harmonyos
三七吃山漆37 分钟前
攻防世界——wzsc_文件上传
android·网络安全·web·ctf