java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling

今天在项目中遇到了这个问题,在此记录下,以防日后遗忘。顺带回忆下Handler机制。

一、先上解决方法

将更新数据的操作使用Handler去执行。

// 你需要确保这个Handler的Looper是主线程的Looper
// 也就是说,如果下列code块是在主线程中执行,请忽略。
// 如果不是,则需要你 Handler handler = new Handler(Looper.getMainLooper());
Handler handler = new Handler();
handler.post(new Runnable() {
    @Override
    public void run() {
        notifyDataSetChanged();
    }
});

二、为什么会出现这个CRASH?</br>

从CRASH的字面看,是因为在RecyclerView layout或scroll 过程中,调用notifyDataSetChanged方法导致的。
而在项目中的具体行为则是:左滑或右滑删除Item的时候,调用了notifyDataSetChanged来刷新数据源。

看看具体CRASH

06-27 16:49:50.463 E/AndroidRuntime(20889): FATAL EXCEPTION: main
06-27 16:49:50.463 E/AndroidRuntime(20889): Process: a, PID: 20889
06-27 16:49:50.463 E/AndroidRuntime(20889): java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:2349)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:4551)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:10366)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:6044)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at a.gui.adapters.TrackAdapters.QueueTrackAdapter$TrackViewHolder.onItemClear(QueueTrackAdapter.java:522)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at a.gui.adapters.swipedrag.SimpleItemTouchHelperCallback.clearView(SimpleItemTouchHelperCallback.java:124)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.helper.ItemTouchHelper.onChildViewDetachedFromWindow(ItemTouchHelper.java:876)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.dispatchChildDetached(RecyclerView.java:6234)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.access$1200(RecyclerView.java:151)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$5.removeViewAt(RecyclerView.java:651)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.ChildHelper.removeViewAt(ChildHelper.java:168)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$LayoutManager.removeViewAt(RecyclerView.java:7092)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$LayoutManager.scrapOrRecycleView(RecyclerView.java:7638)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView$LayoutManager.detachAndScrapAttachedViews(RecyclerView.java:7624)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:546)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at a.gui.activities.QueueActivity$WrapContentLinearLayoutManager.onLayoutChildren(QueueActivity.java:359)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:3260)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3069)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.consumePendingUpdateOperations(RecyclerView.java:1478)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.scrollByInternal(RecyclerView.java:1542)
06-27 16:49:50.463 E/AndroidRuntime(20889):     at android.support.v7.widget.RecyclerView.onTouchEvent(RecyclerView.java:2649)
......
......
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.os.Handler.handleCallback(Handler.java:836)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.os.Handler.dispatchMessage(Handler.java:103)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.os.Looper.loop(Looper.java:203)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.app.ActivityThread.main(ActivityThread.java:6347)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at java.lang.reflect.Method.invoke(Native Method)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)

在RecyclerView中,当RecyclerView的Adapter更新数据时,按照流程会执行assertNotInLayoutOrScroll()这个方法。
assertNotInLayoutOrScroll()这个方法从方法名即可看出,用来检测是否正在layout或者scroll。

    void assertInLayoutOrScroll(String message) {
        if (!isComputingLayout()) {
            if (message == null) {
                throw new IllegalStateException("Cannot call this method unless RecyclerView is "
                        + "computing a layout or scrolling");
            }
            throw new IllegalStateException(message);
        }
    }

在其中,是通过isComputingLayout()判断

    public boolean isComputingLayout() {
        return mLayoutOrScrollCounter > 0;
    }

而mLayoutOrScrollCounter这个变量或者说是标志位会在开始绘制的时候mLayoutOrScrollCounter++;在退出绘制的时候mLayoutOrScrollCounter–;

而从log中看,我的方法onItemClear()正好在绘制流程之中,也就是此时的mLayoutOrScrollCounter是非0的。故而在onItemClear()中直接调用notifyDataSetChanged()方法会CRASH。

三、为什么使用Handler处理就可以?</br>

了解Android消息机制的同学应该知道,Android中的UI的刷新其实是通过消息机制实现的。通过对消息的
分发,从而进行不同处理。

从log中最后几行也可以看出。

......
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.os.Handler.handleCallback(Handler.java:836)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.os.Handler.dispatchMessage(Handler.java:103)
06-27 16:49:50.464 E/AndroidRuntime(20889):     at android.os.Looper.loop(Looper.java:203)
......

通过Handler.post方法将一个Runnable方法块放入MessageQueue[先进先出,后进后出]中去等待执行。而主线程的Looper则会在循环取此队列中的消息。一般情况下,这个流程是串行的。所以当使用Handle.post的时候,UI的更新消息会先被消化掉,等被post的消息被Looper从MessageQueue中取出的时候,【数据源更新】的操作才会被执行。

但是此时RecyclerView的绘制已经完成,mLayoutOrScrollCounter也已经被置为0,故而能够成功更新数据源

    原文作者:菜天Android
    原文地址: https://www.jianshu.com/p/353d401fbd99
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞