RecyclerView:检测到不一致。无效的项目位置——为什么会这样?

5

我们的QA发现了一个错误:发生了以下与RecyclerView相关的崩溃:

java.lang.IndexOutOfBoundsException: 检测到不一致。无效的物品位置2(偏移量:2)。状态:3

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

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

这个bug很容易复现,当滚动速度很快时就会崩溃,但一旦发生就是致命的。

我的工作代码是:

public class MoviesGridRecyclerViewDataAdapter  extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

public static final int VIEW_ITEM = 1;
public static final int VIEW_PROG = -1;
private static final int FADE_DURATION = 500;
int pastVisiblesItems, visibleItemCount, totalItemCount;
private List<VideoEntity> dataList;
private FragmentActivity activity;
private int visibleThreshold = 1;
//private int lastVisibleItem, totalItemCount;
//private boolean loading = false;
private OnLoadMoreListener onLoadMoreListener;
private RecyclerView.OnScrollListener recyclerciewScrollListener;
private RecyclerView recyclerView;
private boolean followLargeItemOnDataRender;
private boolean loading = false;
private boolean isLiveChannel = false;

public MoviesGridRecyclerViewDataAdapter(FragmentActivity fragmentActivity, final List<VideoEntity> dataList, RecyclerView recycler, boolean followLargeItemOnOddData, boolean isLiveChannels) {
    this.dataList = dataList;
    this.activity = fragmentActivity;
    this.followLargeItemOnDataRender = followLargeItemOnOddData;
    this.recyclerView = recycler;
    isLiveChannel = isLiveChannels;

    Log.e("VideoGridRVDataAdapter", "True");

    if (this.recyclerView.getLayoutManager() instanceof GridLayoutManager) {

        final GridLayoutManager gridLayoutManager = (GridLayoutManager) this.recyclerView.getLayoutManager();
        recyclerciewScrollListener = new RecyclerView.OnScrollListener() {

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                if (dy > 0) //check for scroll down
                {
                    visibleItemCount = gridLayoutManager.getChildCount();
                    totalItemCount = gridLayoutManager.getItemCount();
                    pastVisiblesItems = gridLayoutManager.findFirstVisibleItemPosition();

                    if (!loading) {
                        if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
                            loading = true;


                            if (onLoadMoreListener != null) {
                                onLoadMoreListener.onLoadMore(visibleItemCount + pastVisiblesItems);
                            }
                        }
                    }
                }
            }
        };

        this.recyclerView.addOnScrollListener(recyclerciewScrollListener);
    }
}


public void notifyDataLoadingStartProgress() {
    dataList.add(null);
    notifyItemInserted(dataList.size() - 1);
}

public void notifyDataLoaded() {
    dataList.remove(dataList.size() - 1);
    notifyItemRemoved(dataList.size());
    setLoaded();
}


public void resetItems(@NonNull List<VideoEntity> newDataSet) {
    loading = false;
    pastVisiblesItems = visibleItemCount = totalItemCount = 0;

    dataList.clear();
    addItems(newDataSet);
}

public void addItems(@NonNull List<VideoEntity> newDataSetItems) {
    dataList.addAll(newDataSetItems);
    postAndNotifyAdapter(new Handler());
}



public void addItem(VideoEntity item) {
    if (!dataList.contains(item)) {
        dataList.add(item);
        notifyItemInserted(dataList.size() - 1);
    }
}

public void removeItem(VideoEntity item) {
    int indexOfItem = dataList.indexOf(item);
    if (indexOfItem != -1) {
        this.dataList.remove(indexOfItem);
        notifyItemRemoved(indexOfItem);
    }
}

private void postAndNotifyAdapter(final Handler handler) {
    handler.post(new Runnable() {
        @Override
        public void run() {
            if (!recyclerView.isComputingLayout()) {
                try {
                    recyclerView.getRecycledViewPool().clear();
                    notifyDataSetChanged();
                } catch (IndexOutOfBoundsException ex) {
                }

            } else {
                postAndNotifyAdapter(handler);
            }
        }
    });
}

public void destroy() {
    this.recyclerView.removeOnScrollListener(recyclerciewScrollListener);
}


@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
    RecyclerView.ViewHolder vh;
    View v;
    if (viewType == VIEW_PROG) {
        v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_progressbar, viewGroup, false);
        vh = new ProgressViewHolder(v);

    } else {
        Integer layoutResourceId;

        if (isLiveChannel) {
            layoutResourceId = R.layout.item_video_grid_norma_hs_overfow_overdraw;

        } else {
            layoutResourceId = R.layout.item_video_linear_overdraw;

        }
        v = LayoutInflater.from(viewGroup.getContext()).inflate(layoutResourceId, viewGroup, false);
        vh = new MoviesItemRowHolder(this, dataList, v, activity);
    }
    return vh;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {

    if (holder instanceof MoviesItemRowHolder) {
        VideoEntity singleItem = dataList.get(i);
        ((MoviesItemRowHolder) holder).bind(singleItem,dataList);
        //setScaleAnimation(holder.itemView);
    } else {
        ((ProgressViewHolder) holder).progressBar.setIndeterminate(true);
    }
}

public boolean isLoading() {
    return loading;
}

public void setLoaded() {
    loading = false;
}

private void setFadeAnimation(View view) {
    AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
    anim.setDuration(FADE_DURATION);
    view.startAnimation(anim);
}

private void setScaleAnimation(View view) {
    ScaleAnimation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    anim.setDuration(FADE_DURATION);
    view.startAnimation(anim);
}

/**
 * Return the view type of the item at <code>position</code> for the purposes
 * of view recycling.
 * <p/>
 * <p>The default implementation of this method returns 0, making the assumption of
 * a single view type for the adapter. Unlike ListView adapters, types need not
 * be contiguous. Consider using id resources to uniquely identify item view types.
 *
 * @param position position to query
 * @return integer value identifying the type of the view needed to represent the item at
 * <code>position</code>. Type codes need not be contiguous.
 */
@Override
public int getItemViewType(int position) {
    //return position;

    return dataList.get(position) != null ? position : VIEW_PROG;
}

@Override
public int getItemCount() {
    return (null != dataList ? dataList.size() : 0);
}

public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener) {
    this.onLoadMoreListener = onLoadMoreListener;
}

public List<VideoEntity> getItems() {
    return dataList;
}
}

请看一下我的类,并告诉我出了什么问题,当我快速滚动时,在少数设备上会崩溃。
以下是堆栈跟踪:
Fatal Exception: java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 17(offset:17).state:18 android.support.v7.widget.RecyclerView{4266ee78 VFED.... .F....ID 0,0-480,625 #7f0902db app:id/recycler_view_list}, adapter:com.tapmad.tapmadtv.f.e@425896c0, layout:com.tapmad.tapmadtv.WrapContentLinearLayoutManager@424092b0, context:com.tapmad.tapmadtv.activities.SectionMoreActivityHS@4244c478
   at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline + 5923(RecyclerView.java:5923)
   at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition + 5858(RecyclerView.java:5858)
   at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition + 5854(RecyclerView.java:5854)
   at android.support.v7.widget.LinearLayoutManager$LayoutState.next + 2230(LinearLayoutManager.java:2230)
   at android.support.v7.widget.GridLayoutManager.layoutChunk + 557(GridLayoutManager.java:557)
   at android.support.v7.widget.LinearLayoutManager.fill + 1517(LinearLayoutManager.java:1517)
   at android.support.v7.widget.LinearLayoutManager.scrollBy + 1331(LinearLayoutManager.java:1331)
   at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy + 1075(LinearLayoutManager.java:1075)
   at android.support.v7.widget.GridLayoutManager.scrollVerticallyBy + 382(GridLayoutManager.java:382)
   at android.support.v7.widget.RecyclerView.scrollStep + 1832(RecyclerView.java:1832)
   at android.support.v7.widget.RecyclerView$ViewFlinger.run + 5067(RecyclerView.java:5067)
   at android.view.Choreographer$CallbackRecord.run + 791(Choreographer.java:791)
   at android.view.Choreographer.doCallbacks + 591(Choreographer.java:591)
   at android.view.Choreographer.doFrame + 560(Choreographer.java:560)
   at android.view.Choreographer$FrameDisplayEventReceiver.run + 777(Choreographer.java:777)
   at android.os.Handler.handleCallback + 725(Handler.java:725)
   at android.os.Handler.dispatchMessage + 92(Handler.java:92)
   at android.os.Looper.loop + 176(Looper.java:176)
   at android.app.ActivityThread.main + 5317(ActivityThread.java:5317)
   at java.lang.reflect.Method.invokeNative(Method.java)
   at java.lang.reflect.Method.invoke + 511(Method.java:511)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run + 1102(ZygoteInit.java:1102)
   at com.android.internal.os.ZygoteInit.main + 869(ZygoteInit.java:869)
   at dalvik.system.NativeStart.main(NativeStart.java)

任何帮助都非常感谢。谢谢。

你是否已经检查了 https://dev59.com/z-o6XIcBkEYKwwoYFQLr 的答案? - Gustavo
你能贴出onLoadMoreListener的代码吗? - Saeed Entezari
lastHit = lastHitItem; adapter.notifyDataLoadingStartProgress(); new LoadDataUsingAsyncTask().execute(lastHit);最后命中=最后命中项; 适配器.notifyDataLoadingStartProgress(); 新的LoadDataUsingAsyncTask().execute(lastHit); - Jhaman Das
@SaeedEntezari 是的,上面那个是 onLoadMoreListner 代码。 - Jhaman Das
1
我可能没有看到OnLoadMoreListener类,这可能是个问题。为了确认一下,我看到了一个名为MoviesGridRecyclerViewDataAdapter的类。你是否粘贴了OnLoadMoreListener类的实际实现细节? - Saeed Entezari
显示剩余5条评论
5个回答

1
我发现您代码中的问题是,当您清空ArrayList时会出现问题。
dataList.clear();

您必须通知适配器数据集已更改。否则,适配器将不知道数据集的清除,导致其计算出错。

dataList.clear();
notifyDataSetChanged();

因此,RecyclerView适配器将知道数据清除发生了,并且它将重新计算位置。当您添加新数据时,不会出现任何不一致性。

0

每当您像在resetItems()方法中所做的那样操作完整列表时,请调用notifyDataSetChanged();


0

我在创建“加载更多”特性时,我的聊天应用程序遇到了类似的问题,但是我已经成功地通过以下方法解决了这个问题:

  1. 确保当数据到达末尾时,仅调用一次LoadMore以防止过程溢出。 (可以使用布尔切换,比如isNowLoadMore)
  2. 确保在从数据库或Web(后台)获取并编辑数据后,只在UI线程(前台)上通知。 否则,有时可能会产生不一致错误。

当过度调用notify时也会产生不一致错误。


0
将以下代码添加到 onBindViewHolder 方法中。
holder.setIsRecyclable(true);

在设置RecyclerView的适配器之前,使用代码清除过去的位置和视图

recyclerView.getRecycledViewPool().clear();

它之前一直正常工作


在设置适配器之前,使用recyclerView.getRecycledViewPool().clear();清除以前的视图。 - Mohammad Nori
已经调用了postAndNotifyAdapter方法,请检查我的问题。 - Jhaman Das
我知道你检查这段代码,对我来说在每个设置适配器中清除池都有效。 - Mohammad Nori
设置适配器仅在加载更多监听器时调用一次,它会再次获取新数据。 我会刷新我的数据,就像这样。 - Jhaman Das
在哪里放置recyclerView.getRecycledViewPool().clear();? - Jhaman Das
显示剩余2条评论

0

将所有的notifyItemInserted或notifyItemRemoved替换为:notifyDataSetChanged();

public void notifyDataLoadingStartProgress() {
    dataList.add(new VideoEntity());
    notifyDataSetChanged();
}

public void notifyDataLoaded() {
    if (dataList != null && dataList.size() > 1) {
        dataList.remove(dataList.size() - 1);
    }
    setLoaded();
    notifyDataSetChanged();
}

if (dy > 0) {//check for scroll down
    visibleItemCount = gridLayoutManager.getChildCount();
    totalItemCount = gridLayoutManager.getItemCount();
    pastVisiblesItems = gridLayoutManager.findFirstVisibleItemPosition();
    if (loading) {   //<<-----------
        if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
            loading = false;   //<<-----------
            if (onLoadMoreListener != null) {
                onLoadMoreListener.onLoadMore(visibleItemCount + pastVisiblesItems);
            }
        }
    }
}

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