RecyclerView.RecycledViewPool+BaseQuickAdapter+FooterLayout造成IllegalStateException

一、RecyclerView有一个优化设置,如果多个RecyclerView拥有相同的item布局,可以通过使用一个RecycledViewPool来减少缓存的View数目。

    public void setRecycledViewPool(@Nullable RecycledViewPool pool) {
        mRecycler.setRecycledViewPool(pool);
    }

Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. This can be useful if you have multiple RecyclerViews with adapters that use the same view types, for example if you have several data sets with the same kinds of item views displayed by a ViewPager.
Params: pool – Pool to set. If this parameter is null a new pool will be created and used.

一直没有机会用,终于伟大的产品经理给了我一个机会:
在这里插入图片描述
实现四个横向列表。

二、掉进坑里

RecyclerView的apdater使用了BaseRecyclerViewAdapterHelper的BaseQuickAdapter,由于最后一项是与数据元素不同的查看更多元素,简单通过BaseQuickAdapter的FooterLayout实现。但实现后,偶遇了 java.lang.IllegalStateException: ViewHolder views must not be attached when created. Ensure that you are not passing ‘true’ to the attachToRoot parameter of LayoutInflater.inflate(…, boolean attachToRoot)

java.lang.IllegalStateException: ViewHolder views must not be attached when created. Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot)
        at androidx.recyclerview.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:6796)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5975)
        at androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:286)
        at androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:343)
        at androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:359)
        at androidx.recyclerview.widget.GapWorker.prefetch(GapWorker.java:366)
        at androidx.recyclerview.widget.GapWorker.run(GapWorker.java:397)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:207)
        at android.app.ActivityThread.main(ActivityThread.java:6867)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

Scrapped or attached views may not be recycled. isScrap:false isAttached:true androidx.recyclerview.widget.RecyclerView{afcaf63 VFED… … 0,79-1080,726 #7f0905a2 app:id/recycler_view}

java.lang.IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true androidx.recyclerview.widget.RecyclerView{afcaf63 VFED..... ........ 0,79-1080,726 #7f0905a2 app:id/recycler_view}, adapter:com.ss.android.tuchong.feed.model.FindCircleAdapter@4990760, layout:androidx.recyclerview.widget.LinearLayoutManager@9a79419, context:com.ss.android.tuchong.main.controller.MainActivity@108a246
        at androidx.recyclerview.widget.RecyclerView$Recycler.recycleViewHolderInternal(RecyclerView.java:6159)
        at androidx.recyclerview.widget.RecyclerView$Recycler.recycleView(RecyclerView.java:6103)
        at androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:293)
        at androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:343)
        at androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:359)
        at androidx.recyclerview.widget.GapWorker.prefetch(GapWorker.java:366)
        at androidx.recyclerview.widget.GapWorker.run(GapWorker.java:397)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:207)
        at android.app.ActivityThread.main(ActivityThread.java:6867)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

多次尝试,发现复现的概率不低。

三、原因

通过异常栈可以看到,Adpater的createViewHolder中

        public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
            try {
                TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
                final VH holder = onCreateViewHolder(parent, viewType);
                if (holder.itemView.getParent() != null) {
                    throw new IllegalStateException("ViewHolder views must not be attached when"
                            + " created. Ensure that you are not passing 'true' to the attachToRoot"
                            + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
                }
                holder.mItemViewType = viewType;
                return holder;
            } finally {
                TraceCompat.endSection();
            }
        }

可以看到,是判断onCreateViewHolder()返回的holder的itemView的parent不为空时,抛出的异常。

3.1 尝试简单修复

最初的想法是onCreateViewHolder()方法我是可以接触到的,可以在方法返回前处理

val oldParent = viewHolder.itemView.parent
if (oldParent is ViewGroup) {
    oldParent.removeView(viewHolder.itemView)
}

崩溃没有了,但发现修复前和修复后都有点击查看全部按钮响应错位的情况出现。
在这里插入图片描述

3.2 查找原因

所以找不到真正原因的盲目修复,通常不能准确解决问题。必须要找到真正原因才能准确修复。要找到真正原因有多种方法,一种是多种角度尝试,尝试找到稳定复现规律;还有一种是增加日志,获取到崩溃时更多的信息;还有一种方法是设断点debug,但未找到稳定复现规律时,盲目debug效率很低;最厉害也是要求最高的方法就是读源码分析了。
首先,采用的是增加日志的方式,尝试获取崩溃时,onCreateViewHolder()中viewHolder的viewType

val oldParent = viewHolder.itemView.parent
if (oldParent is ViewGroup) {
    LogcatUtils.e("old parent is not null ${parent.javaClass.canonicalName} remove view $viewType")
    oldParent.removeView(viewHolder.itemView)
}

发现出现问题时viewHolder的type为819

old parent is not null androidx.recyclerview.widget.RecyclerView remove view 819 threadId:2

在BaseQuickAdapter中查下,viewType为819的是FOOTER_VIEW

    public static final int FOOTER_VIEW = 0x00000333;// (256+16+1)*3=273*3=819

看下BaseQuickAdapter中添加FooterView及使用的方法,从addFooterView(View)入手。

    public int addFooterView(View footer) {
        return addFooterView(footer, -1, LinearLayout.VERTICAL);
    }
    public int addFooterView(View footer, int index) {
        return addFooterView(footer, index, LinearLayout.VERTICAL);
    }

都是调用它的重载方法,真正添加FooterView的方法是

    public int addFooterView(View footer, int index, int orientation) {
        if (mFooterLayout == null) {
            mFooterLayout = new LinearLayout(footer.getContext());
            if (orientation == LinearLayout.VERTICAL) {
                mFooterLayout.setOrientation(LinearLayout.VERTICAL);
                mFooterLayout.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
            } else {
                mFooterLayout.setOrientation(LinearLayout.HORIZONTAL);
                mFooterLayout.setLayoutParams(new LayoutParams(WRAP_CONTENT, MATCH_PARENT));
            }
        }
        final int childCount = mFooterLayout.getChildCount();
        if (index < 0 || index > childCount) {
            index = childCount;
        }
        mFooterLayout.addView(footer, index);
        if (mFooterLayout.getChildCount() == 1) {
            int position = getFooterViewPosition();
            if (position != -1) {
                notifyItemInserted(position);
            }
        }
        return index;
    }

创建了一个LinearLayout的作为mFooterLayout,然后将footerView添加到mFooterLayout中,然后通知RecyclerView刷新。
RecyclerView如何将mFooterLayout与ViewHolder绑定到一起是在BaseQuickAdapter的onCreateViewHolder(ViewGroup,int)中

    @Override
    public K onCreateViewHolder(ViewGroup parent, int viewType) {
        K baseViewHolder = null;
        this.mContext = parent.getContext();
        this.mLayoutInflater = LayoutInflater.from(mContext);
        switch (viewType) {
            ...// 省略
            case FOOTER_VIEW:
                baseViewHolder = createBaseViewHolder(mFooterLayout);
                break;
            default:
                baseViewHolder = onCreateDefViewHolder(parent, viewType);
                bindViewClickListener(baseViewHolder);
        }
        baseViewHolder.setAdapter(this);
        return baseViewHolder;
    }
    
    protected K createBaseViewHolder(View view) {
        Class temp = getClass();
        Class z = null;
        while (z == null && null != temp) {
            z = getInstancedGenericKClass(temp);
            temp = temp.getSuperclass();
        }
        K k;
        // 泛型擦除会导致z为null
        if (z == null) {
            k = (K) new BaseViewHolder(view);
        } else {
            k = createGenericKInstance(z, view);
        }
        return k != null ? k : (K) new BaseViewHolder(view);
    }

因此对于AdapterA而言,mFooterLayoutA为其内部变量,会有一个BaseViewHolderA与其绑定,当滑动RecyclerViewA使得Footer完全隐藏,RecyclerViewA会将mFooterLayoutA移除,BaseViewHolderA也被回收;
如果此时滑动RecyclerViewB,去显示Footer,由于可以从复用池(RecycledViewPool)中复用BaseViewHolderA,mFooterLayoutA会成为RecyclerViewB的child。如果此时滑动RecyclerViewA去尝试显示Footer,由于复用池中没有缓存的Footer对应的BaseViewHolder,会重新走onCreateViewHolder(ViewGroup,int)方法,进而直接使用mFooterLayoutA去创建BaseViewHolderB,而此时mFooterLayoutA的parent为RecyclerViewB,那么就会在之后的判断处抛出异常。

                final VH holder = onCreateViewHolder(parent, viewType);
                if (holder.itemView.getParent() != null) {
                    throw new IllegalStateException("ViewHolder views must not be attached when"
                            + " created. Ensure that you are not passing 'true' to the attachToRoot"
                            + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");

这也是为什么会出现点击错乱的情况。

3.3问题处理

这个就不光彩了,因为最简单的处理方法就是不公用RecycledViewPool。如果直接使用RecyclerView.Adapter,多RecyclerView公用RecycledViewPool并无问题,但由于BaseQuickAdapter做了自己的封装,导致会出现问题。

感悟:封装会简便实现,但不当的封装也会丢失底层框架的优化特性!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章