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

相关推荐
2501_9160074719 小时前
iOS 混淆工具链实战,多工具组合完成 IPA 混淆与加固(iOS混淆|IPA加固|无源码混淆|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
Jeled21 小时前
Retrofit 与 OkHttp 全面解析与实战使用(含封装示例)
android·okhttp·android studio·retrofit
ii_best1 天前
IOS/ 安卓开发工具按键精灵Sys.GetAppList 函数使用指南:轻松获取设备已安装 APP 列表
android·开发语言·ios·编辑器
2501_915909061 天前
iOS 26 文件管理实战,多工具组合下的 App 数据访问与系统日志调试方案
android·ios·小程序·https·uni-app·iphone·webview
limingade1 天前
手机转SIP-手机做中继网关-落地线路对接软交换呼叫中心
android·智能手机·手机转sip·手机做sip中继网关·sip中继
RainbowC01 天前
GapBuffer高效标记管理算法
android·算法
程序员码歌1 天前
豆包Seedream4.0深度体验:p图美化与文生图创作
android·前端·后端
、花无将1 天前
PHP:下载、安装、配置,与apache搭建
android·php·apache
shaominjin1231 天前
Android 约束布局(ConstraintLayout)的权重机制:用法与对比解析
android·网络
我命由我123451 天前
Android 对话框 - 对话框全屏显示(设置 Window 属性、使用自定义样式、继承 DialogFragment 实现、继承 Dialog 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime