今天在项目中遇到了这个问题,在此记录下,以防日后遗忘。顺带回忆下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,故而能够成功更新数据源