使用RecyclerView遇到的一些问题 Inconsistency detected

来源:互联网 发布:单词社交网络高级版pdf 编辑:程序博客网 时间:2024/05/17 10:06
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{424b7690 position=7 id=-1, oldPos=8,pLpos:8 scrap tmpDetached no parent} at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:4349)

以及

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position   157(offset:157).state:588

这个问题在google官方话题单有提到,地址为
ISSUE 77846

其中,项目的pm 有提及如下两点说明:

1.ListView and RecyclerView are different

ListView and RecyclerView are different. RecyclerView is designed to work with different components and makes certain promises. For the error above, exception happens when LayoutManager tries to get a View for a position. A count is already provided to the LayoutManager at the beginning of the layout, this is a promise and guaranteed not to change until the layout is complete. RecyclerView cannot say “you have 6 items” and when LayoutManager asks for item at position 5, return null. LayoutManager might have done its own calculations based on that count, doing so may leave its state unstable. On the other hand, ListView has full control, thus it can forgive these things. Besides that, ListView does not do anything clever about the adapter contents whereas RecyclerView does a lot to support animations. The two are fairly different. To be honest, if you do not dispatch detailed notify events, there is little to no benefit on moving to RecyclerView. If you dispatch them, it will both help UX and performance. (e.g. avoiding unnecessary rebinds)

RecyclerView throws an exception because problem happens due to a developer error and should be fixed. If it is a RecyclerView, we have to fix it. In both cases, for a consistent and stable API, forgiving developer errors (both ends) is not a sustainable solution.

About adapter count, that getItemCount is one API I regret leaving public (was an old API, had to be kept for some backward compatibility).
LayoutManagers are expected to get item count from the State. If you check framework layout managers, all work w/ state. There is a strict abstraction between the Adapter and LayoutManager (due to animations). Even for notify events, RecyclerVIew re-writes them (in a consistent way) to suit them for two pass animations. There is a lot going on there, hard to explain here. (see docs: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.State.html#getItemCount())
RecyclerView also provides an API to convert layout positions to adapter positions if necessary.
convertPreLayoutPositionToPostLayout : https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Recycler.html#convertPreLayoutPositionToPostLayout(int)

The trigger for the bug might be events while RV is detached, some Runnable may not be running due to View being detached. Some info / logs would be very helpful so that i can create a test case and fix it.
Thanks.


2.let RV know about it

Great to hear that issue is fixed. I did not understand why you are calling notifyInserted w/o inserting them. It will definitely create a problem. This explains why RV expects to have more items in the adapter.
Only call these events right after you change the data. (has to be in the same call stack ~ main looper loop~)

Notify events are handled asynchronously. So you can call as many notify events as you want and RV will handle all of them in the next layout pass.It batches them etc. You just need to guarantee that all of them are consistent with each other. That is, in every step you change adapter, you should let RV know about it. Your events should be consistent. For example, if you want to remove first two elements 1 by one, you should call:

mData.removeItemAt(0);
notifyItemRemoved(0);
mData.removeItemAt(0);
notifyItemRemoved(0);

A common mistake would be to think that you need to call notifyItemRemoved(0); then notifyItemRemoved(1);. This is NOT true as RV knows items will shift if first item is removed. This is also consistent w/ what you would do while handling a list.

This is the simplest way to get them right. Technically, you can let RV know right after you update the backing data, as long as you do it in the same call stack.
e.g.
mData.removeItemAt(0);
mData.removeItemAt(0);
notifyItemRangeRemoved(0, 2); //2 items, starting from 0.

So your code probably works fine if notifyDataSetChanged arrives before the next layout calculation but fails otherwise.

When you call notifyDataSetChanged, you void all previous notify events (in that frame). Don’t call notifyDataSetChanged if you don’t have to.

Good luck and thanks for the update, I’m closing the issue.


第一点,阐述了RV(RecyclerView)和ListView的区别。如果不涉及动画等特效,PM是建议大家使用ListView的。RecyclerView使用的关键在于“动态”绑定了数据,通过LayoutManager来进行数据的相关界面展示。也就是说,如果我当前RV的数据变化不当时(即开发者使用不当时候)会造成RV的错误。PM顺便吐槽了下,出了问题也不能只怪我们开发组,有时候是你开发者的问题(囧–!)。
我的项目需求是实现一个类似Gallery的相册。在使用RV时,高度并不能wrap_content,为此,我复写了LayoutManager。因此,在

public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {....}

方法中需要测量出RV的子控件的宽高进行界面的适配。通过

    Logger.d("state:" + state.toString());

可以看到state包含如下数据

state:State{mTargetPosition=-1, mPreLayoutHolderMap={}, mPostLayoutHolderMap={}, mData=null, mItemCount=6, mPreviousLayoutItemCount=6, mDeletedInvisibleItemCountSincePreviousLayout=0, mStructureChanged=true, mInPreLayout=false, mRunSimpleAnimations=false, mRunPredictiveAnimations=false}

但遗憾的是state并没有完全提供这些数据的get方法。
如PM所说,其实state已经包含了我们需要的一系列的信息,其实可以避免使用getItemCount()方法来获取当前的子数据数量的。

第二点,提出了解决遇到这些问题的关键原因。
在进行数据移除和数据增加时,务必要保证RVAdapter中的数据和移除的数据保持一致!
此外,不建议使用notifyDataSetChanged 方法,因为这会让美好的效果通通木有了。
个人认为,如果使用了notifyDataSetChanged ,那还不如使用listview了。

我的项目中遇到的问题是,多选删除后,会报Inconsistency detected的错误,结合在进行数据移除和数据增加时,务必要保证RVAdapter中的数据和移除的数据保持一致!来进行排查,发现了问题所在:从服务器获取到删除图片成功的返回值后,先进行了data的清除,然后在主线程里面又结合notifyItemRemoved重新删除了一次!

我项目中报过好几次Inconsistency detected错误,因此,在遇到相关问题时,应该第一时间切记数据源和RV变动数据的同步。当然,这个bug也可能是其他原因导致的,如果有,我再更新上来。

1 0