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做了自己的封裝,導致會出現問題。

感悟:封裝會簡便實現,但不當的封裝也會丟失底層框架的優化特性!

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