RecyclerView:检测到不一致。无效的项目位置。

343

我们的QA检测到一个bug: 在旋转安卓设备(Droid Turbo)时,发生了以下RecyclerView相关崩溃:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 2(offset:2).state:3

在我看来,这似乎是RecyclerView内部错误,因为我想不出任何直接由我们的代码引起此问题的方式...

有人遇到过这个问题吗?

解决方案是什么?

一个暴力的解决方法可能是在发生异常时捕获异常并从头重新创建RecyclverView实例,以避免留下已损坏的状态。

但是,如果可能的话,我希望更好地理解问题(也许在源头上修复它),而不是掩盖它。

这个bug不容易复现,但一旦发生就是致命的。

完整的堆栈跟踪:

W/dalvikvm( 7546): threadid=1: thread exiting with uncaught exception (group=0x41987d40)
    E/AndroidRuntime( 7546): FATAL EXCEPTION: main
    E/AndroidRuntime( 7546): Process: com.oblong.mezzedroid, PID: 7546
    E/AndroidRuntime( 7546): java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 2(offset:2).state:3
    E/AndroidRuntime( 7546):    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3382)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3340)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1810)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1306)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1269)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:523)
    E/AndroidRuntime( 7546):    at org.liboid.recycler_view.RecyclerViewContainer$LiLinearLayoutManager.onLayoutChildren(RecyclerViewContainer.java:179)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:1942)
    E/AndroidRuntime( 7546):    at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:2237)
    E/AndroidRuntime( 7546):    at org.liboid.recycler_view.LiRecyclerView.onLayout(LiRecyclerView.java:30)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
    E/AndroidRuntime( 7546):    at com.oblong.mezzedroid.workspace.content.bins.BinsContainerLayout.onLayout(BinsContainerLayout.java:22)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
    E/AndroidRuntime( 7546):    at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    E/AndroidRuntime( 7546):    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    E/AndroidRuntime( 7546):    at android.view.View.layout(View.java:14946)
    E/AndroidRuntime( 7546):    at android.view.ViewGroup.layout(ViewGroup.java:4651)
    E/AndroidRuntime( 7546):    at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2132)
    E/AndroidRuntime( 7546):    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1872)
    E/AndroidRuntime( 7546):    at andro

2
一个问题:你的复现有多一致?我知道这是谷歌代码中的错误在这里在这里。但这可以避免。那么,这是每次旋转都会发生吗? - Vic Vuci
1
嗨。这种情况很少发生,但一旦发生,对应用程序来说是致命的。 - KarolDepka
1
如果您能轻松地再现此问题,我建议在调用“notify*”之前打印“getItemCount”的值...您可能会发现您的项目计数与您的假设不符。 - Rich Ehmer
3
需要记住的规则是:“在更改数据集之后立即调用notify...(),并确保从同一线程调用它”。 - Richard Le Mesurier
3
如果您调用了adapter.setHasStableIds(),只需移除这个调用即可。这对我很有效。 - alpha
显示剩余7条评论
51个回答

267

我遇到了一个(可能)相关的问题 - 使用RecyclerView创建一个新的活动实例,但使用较小的适配器会导致崩溃。

RecyclerView.dispatchLayout()在调用mRecycler.clearOldPositions()之前尝试从废弃物中提取项目。其后果是它从公共池中提取了位置高于适配器大小的项目。

幸运的是,只有在启用PredictiveAnimations时才会这样做,因此我的解决方案是子类化GridLayoutManagerLinearLayoutManager也存在相同的问题和解决方法),并重写supportsPredictiveItemAnimations()返回false:

/**
 * No Predictive Animations GridLayoutManager
 */
private static class NpaGridLayoutManager extends GridLayoutManager {
    /**
     * Disable predictive animations. There is a bug in RecyclerView which causes views that
     * are being reloaded to pull invalid ViewHolders from the internal recycler stack if the
     * adapter size has decreased since the ViewHolder was recycled.
     */
    @Override
    public boolean supportsPredictiveItemAnimations() {
        return false;
    }

    public NpaGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public NpaGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
    }

    public NpaGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
    }
}

5
这对我有用,禁用预测动画不会让你完全失去动画效果。太棒了。 - Bob Liberatore
8
非常感谢您先生!使用LinearLayoutManager立即起效,可能节省了我数天时间。 - levavare
8
非常感谢。这个解决方案适用于LinearLayoutManager。 - Pruthviraj
11
我认为这个人应该得到我们建立一座纪念雕像以表彰他宝贵的帮助...... 这是网络上文档最差的问题之一,但似乎很多开发者都遇到了这个问题...... 我只是想知道你是如何发现如果 PredictiveAnimations 为 false 就可以跳过它的,@KasHunt?因为堆栈跟踪非常不清楚... - PAD
8
有人知道如何在不使用这种方法的情况下修复它吗?因为notifyDatasetChanged已经被DiffUtil所取代。 - Anton Shkurenko
显示剩余15条评论

113
在我的情况下(删除/插入数据到我的数据结构中),我需要清空回收池并通知数据集已更改! mRecyclerView.getRecycledViewPool().clear(); mAdapter.notifyDataSetChanged();

10
我通常不这么说,但是非常感谢你。我已经尝试了所有方法来解决这个问题,每当我快速移动列表中的一堆项目时,它就会偶尔崩溃。我可能花费了整整一周的时间来解决这个问题。我离开了它几个月,试图让我的大脑以不同的方式来接近它,然后在第一次谷歌搜索时找到了这个。谢谢你! - Chantell Osejo
为什么你必须这样做? - dabluck
21
那是一项相当繁重的操作,有些违背了回收利用的初衷。 - gjsalot
@gjsalot,如果我使用这个,会引起一些问题吗? - Sreekanth Karumanaghat
2
如果我使用MVVM模型,我应该在哪里编写这段代码? - Nadeem Shaikh

33

在这种情况下,请使用notifyDataSetChanged()而不是notifyItem...


5
在某些情况下,这是正确的方式。我曾经遇到过这样的情况:我更换了所有的物品,但我没有对适配器说实话,只告诉它我插入了一些新项目(使用 notifyItemRangeInserted 方法),而没有先告诉它我也删除了一些项目。然后适配器期望存在比实际数量更多的项目。如果使用适配器的任何通知方法(除了 notifyDataSetChanged),例如notifyItemRangeRemoved/Inserted/Updated,调用者有完全的责任向适配器精确地说明发生了什么变化,否则您可能会遇到这种“不一致状态”。 - JHH
45
这根本不是解决方案。 - Miha_x64
这不是正确的方法。如果这样做可以工作,那么这意味着您刚刚搞乱了 notifyItem... 的范围,并且纠正它将开始工作,而不是重新渲染所有项目。 - Ranjan
@Ranjan RecyclerView 不会“重新渲染所有项目”,它只会渲染屏幕上(“可见”)的内容。 - Farid

26

我也遇到了与RecyclerView相同的问题,所以在清除列表后,我立即通知适配器进行数据集更改。

mList.clear();
mAdapter.notifyDataSetChanged();

mList.addAll(newData);
mAdapter.notifyDataSetChanged();

20

当用户滚动时,如果适配器中的列表被清空,就会出现此错误,这会使得项持有者的位置发生变化,在UI上丢失列表和项之间的引用,错误会在下一个"notifyDataSetChanged"请求中发生。

修复方法:

检查您的更新列表方法。如果您执行类似以下操作:

mainList.clear();
...
mainList.add() or mainList.addAll()
...
notifyDataSetChanged();

===> Error occur

如何修复。创建一个新的列表对象进行缓冲处理,并在此之后将其重新分配给主列表。

List res = new ArrayList();
…..
res.add();  //add item or modify list
….
mainList = res;
notifyDataSetChanged();

感谢 Nhan Cao 的帮助 :)


15
我通过延迟 mRecycler.setAdapter(itemsAdapter) 直到将所有项目添加到适配器中的 mRecycler.addAll(items) 来解决了这个问题,它有效了。我不知道为什么一开始要这样做,这是从一个库的代码中看到的那些“错误的顺序”。我相当确定这就是答案,请确认并解释原因吗?不确定这是否是有效的答案。

我认为这是解决方案,一旦我延迟了适配器,它就没问题了...现在当在UI线程中设置适配器并向其添加项目时,它会弹出。 - EngineSense
20
我使用了swapAdapter(adapter, true)代替setAdapter(adapter),这有所帮助。 - frangulyan

13

我有同样的问题。当我快速滚动、调用API和更新数据时,它就会出现。尝试了所有事情来防止崩溃后,我找到了解决方案。

mRecyclerView.stopScroll();

它会起作用。


3
这是一个解决方法而不是一个修复方法。你正在强制停止滚动。这会导致糟糕的用户体验。 - Cyph3rCod3r
1
@Dr.aNdRO:适配器需要设置位置,如果您继续滚动Recylerview,则适配器无法设置数据,这是崩溃的原因。这并不是糟糕的用户体验。 - Anand Savjani
1
停止滚动并不是不好的用户体验,因为数据正在刷新。 - Sush
1
谢谢。这应该是被接受的答案@AnandSavjani。 - PrakashExplorer

12

我有一个类似的问题,但不完全相同。在我的情况下,在某一点上,我清空了传递给RecyclerView的数组。

mObjects.clear();

我没有调用notifyDataSetChanged,因为我不想让Recyclerview立即清除视图。我正在使用AsyncTask重新填充mObjects数组。


7

对我而言,只需添加以下代码行即可:

mRecyclerView.setItemAnimator(null);

2
在大多数情况下,这不是一个解决方案。如果您想要动画效果,您需要重写适配器代码并查找通知更改的错误。 - Dragos Rachieru
它对我来说运行良好。我正在使用真实的适配器,所以我无法控制流程,并且我在样式中启用了windowActivityTransitions,这就是导致此问题的原因。谢谢你,你救了我的一天。 - Arul

6
我正在后台 Thread 中修改 RecyclerView 的数据。我遇到了与 OP 相同的 Exception。我在改变数据之后添加了以下内容:
myRecyclerView.post(new Runnable() {
    @Override
    public void run() {
        myRecyclerAdapter.notifyDataSetChanged();
    }
});

希望这能有所帮助。


谢谢你!这是唯一一个从Android开发角度来看有意义的答案。 - philtz
虽然我也用 view.recycler_view.post 的帮助解决了问题,但我使用了 notifyItemInserted。在我的情况下,它已经是 UI 线程了。 - CoolMind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接