不当使用BaseQuickAdapter和notifyItemRemoved出现IndexOutOfBoundsException

1.描述:RecyclerView BaseQuickAdapter notifyItemRemoved()在使用EmptyView,并且有headerView时,删除列表中唯一一个元素会崩溃

2.崩溃详情:

java.lang.IndexOutOfBoundsException

Inconsistency detected. Invalid view holder adapter positionViewHolder{9b31b49 position=1 id=-1, oldPos=2, pLpos:2 scrap [attachedScrap] tmpDetached no parent} android.support.v7.widget.RecyclerView{bf807a VFED.V... .F....ID 0,0-1080,1773 #7f0f0384 app:id/user_blogs_recycler_view}, adapter:com.ss.android.tuchong.mine.model.UserWorksListAdapter@7c23489, layout:android.support.v7.widget.GridLayoutManager@4a84c8e, context:com.ss.android.tuchong.main.controller.MainActivity@35eaf9b

1 android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5610)

2 android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5792)

3 android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5752)

4 android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5748)

5 android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2232)

6 android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:556)

7 android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1519)

8 android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:614)

9 android.support.v7.widget.GridLayoutManager.onLayoutChildren(GridLayoutManager.java:170)

10 android.support.v7.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3763)

11 android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3527)

12 android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:4082)

13 android.view.View.layout(View.java:19612)

14 android.view.ViewGroup.layout(ViewGroup.java:6055)

15 android.support.v4.widget.SwipeRefreshLayout.onLayout(SwipeRefreshLayout.java:606)

16 android.view.View.layout(View.java:19612)

17 android.view.ViewGroup.layout(ViewGroup.java:6055)

18 android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)

19 android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1780)

20 android.widget.LinearLayout.onLayout(LinearLayout.java:1546)

21 android.view.View.layout(View.java:19612)

22 android.view.ViewGroup.layout(ViewGroup.java:6055)

23 android.support.v4.view.ViewPager.onLayout(ViewPager.java:1769)

24 android.view.View.layout(View.java:19612)

25 android.view.ViewGroup.layout(ViewGroup.java:6055)

26 android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)

27 android.widget.FrameLayout.onLayout(FrameLayout.java:261)

28 android.view.View.layout(View.java:19612)

29 android.view.ViewGroup.layout(ViewGroup.java:6055)

30 android.widget.RelativeLayout.onLayout(RelativeLayout.java:1080)

31 com.ss.android.tuchong.common.view.HeaderViewPager.onLayout(HeaderViewPager.java:315)

32 android.view.View.layout(View.java:19612)

33 android.view.ViewGroup.layout(ViewGroup.java:6055)

34 android.widget.RelativeLayout.onLayout(RelativeLayout.java:1080)

35 android.view.View.layout(View.java:19612)

36 android.view.ViewGroup.layout(ViewGroup.java:6055)

37 android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)

38 android.widget.FrameLayout.onLayout(FrameLayout.java:261)

39 android.view.View.layout(View.java:19612)

40 android.view.ViewGroup.layout(ViewGroup.java:6055)

41 android.widget.RelativeLayout.onLayout(RelativeLayout.java:1080)

42 android.view.View.layout(View.java:19612)

43 android.view.ViewGroup.layout(ViewGroup.java:6055)

44 android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)

45 android.widget.FrameLayout.onLayout(FrameLayout.java:261)

46 android.view.View.layout(View.java:19612)

47 android.view.ViewGroup.layout(ViewGroup.java:6055)

48 android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)

49 android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)

50 android.widget.LinearLayout.onLayout(LinearLayout.java:1544)

51 android.view.View.layout(View.java:19612)

52 android.view.ViewGroup.layout(ViewGroup.java:6055)

53 android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)

54 android.widget.FrameLayout.onLayout(FrameLayout.java:261)

55 android.view.View.layout(View.java:19612)

56 android.view.ViewGroup.layout(ViewGroup.java:6055)

57 android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)

58 android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)

59 android.widget.LinearLayout.onLayout(LinearLayout.java:1544)

60 android.view.View.layout(View.java:19612)

61 android.view.ViewGroup.layout(ViewGroup.java:6055)

62 android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)

63 android.widget.FrameLayout.onLayout(FrameLayout.java:261)

64 com.android.internal.policy.DecorView.onLayout(DecorView.java:761)

65 android.view.View.layout(View.java:19612)

66 android.view.ViewGroup.layout(ViewGroup.java:6055)

67 android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2519)

68 android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2235)

69 android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1421)

70 android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6842)

71 android.view.Choreographer$CallbackRecord.run(Choreographer.java:1026)

72 android.view.Choreographer.doCallbacks(Choreographer.java:838)

73 android.view.Choreographer.doFrame(Choreographer.java:769)

74 android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1012)

75 android.os.Handler.handleCallback(Handler.java:789)

76 android.os.Handler.dispatchMessage(Handler.java:98)

77 android.os.Looper.loop(Looper.java:171)

78 android.app.ActivityThread.main(ActivityThread.java:6699)

79 java.lang.reflect.Method.invoke(Native Method)

80 com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:246)

81 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)

 

3.异常发生点:

       /**
         * Helper method for getViewForPosition.
         * 

* Checks whether a given view holder can be used for the provided position. * * @param holder ViewHolder * @return true if ViewHolder matches the provided position, false otherwise */ boolean validateViewHolderForOffsetPosition(ViewHolder holder) { // if it is a removed holder, nothing to verify since we cannot ask adapter anymore // if it is not removed, verify the type and id. if (holder.isRemoved()) { if (DEBUG && !mState.isPreLayout()) { throw new IllegalStateException("should not receive a removed view unless it" + " is pre layout" + exceptionLabel()); } return mState.isPreLayout(); } if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " + "adapter position" + holder + exceptionLabel()); } if (!mState.isPreLayout()) { // don't check type if it is pre-layout. final int type = mAdapter.getItemViewType(holder.mPosition); if (type != holder.getItemViewType()) { return false; } } if (mAdapter.hasStableIds()) { return holder.getItemId() == mAdapter.getItemId(holder.mPosition); } return true; }

其中holder是RecyclerView.BaseViewHolder类,它的toString()方法

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("ViewHolder{"
                    + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId
                    + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition);
            if (isScrap()) {
                sb.append(" scrap ")
                        .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]");
            }
            if (isInvalid()) sb.append(" invalid");
            if (!isBound()) sb.append(" unbound");
            if (needsUpdate()) sb.append(" update");
            if (isRemoved()) sb.append(" removed");
            if (shouldIgnore()) sb.append(" ignored");
            if (isTmpDetached()) sb.append(" tmpDetached");
            if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")");
            if (isAdapterPositionUnknown()) sb.append(" undefined adapter position");

            if (itemView.getParent() == null) sb.append(" no parent");
            sb.append("}");
            return sb.toString();
        }

 

4.原因查找:

根据崩溃信息

Invalid view holder adapter positionViewHolder{9b31b49 position=1 id=-1, oldPos=2, pLpos:2 scrap [attachedScrap] tmpDetached no parent} android.support.v7.widget.RecyclerView{bf807a VFED.V... .F....ID 0,0-1080,1773 #7f0f0384 app:id/user_blogs_recycler_view}, adapter:com.ss.android.tuchong.mine.model.UserWorksListAdapter@7c23489, layout:android.support.v7.widget.GridLayoutManager@4a84c8e, context:com.ss.android.tuchong.main.controller.MainActivity@35eaf9b

和崩溃代码

if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
    + "adapter position" + holder + exceptionLabel());
}

可知,holder.mPosition为1,所以holder.mPosition >= mAdapter.getItemCount()为true,看下BaseQuickAdapter的getItemCount()方法

    @Override
    public int getItemCount() {
        int count;
        if (getEmptyViewCount() == 1) {
            count = 1;
            if (mHeadAndEmptyEnable && getHeaderLayoutCount() != 0) {
                count++;
            }
            if (mFootAndEmptyEnable && getFooterLayoutCount() != 0) {
                count++;
            }
        } else {
            count = getHeaderLayoutCount() + mData.size() + getFooterLayoutCount() + getLoadMoreViewCount();
        }
        return count;
    }

其中,在使用EmptyView的情况下,getEmptyViewCount()方法返回1。

   /**
     * if show empty view will be return 1 or not will be return 0
     *
     * @return
     */
    public int getEmptyViewCount() {
        if (mEmptyLayout == null || mEmptyLayout.getChildCount() == 0) {
            return 0;
        }
        if (!mIsUseEmpty) {
            return 0;
        }
        if (mData.size() != 0) {
            return 0;
        }
        return 1;
    }

那么,在添加了headerView又通过data.remove(0)将最后一个元素删除时,count = getHeaderLayoutCount() + mData.size() + getFooterLayoutCount() + getLoadMoreViewCount();

如果没有footer、loadMore的情况下,count为1。这样就符合崩溃条件,导致崩溃了。

5.解决方案:

一开始想用如下方案,没有问题了

if (mAdapter.data.size == 0) {
    mAdapter.notifyDataSetChanged()
} else {
    mAdapter.notifyItemRemoved(mAdapter.headerLayoutCount + index)
}

但看了下BaseQuickAdapter的源码,发现有remove(@IntRange(from = 0) int position)方法

   /**
     * remove the item associated with the specified position of adapter
     *
     * @param position
     */
    public void remove(@IntRange(from = 0) int position) {
        mData.remove(position);
        int internalPosition = position + getHeaderLayoutCount();
        notifyItemRemoved(internalPosition);
        compatibilityDataSizeChanged(0);
        notifyItemRangeChanged(internalPosition, mData.size() - internalPosition);
    }

试了下,也没有问题,第一次发现adapter的notify()可以这样用,BaseQuickAdapter也是做到极致了。

你可能感兴趣的:(android)