一、问题
在使用RecyclerView时发生了java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling崩溃,从崩溃提示来看,是因为在使用RecyclerView过程中进行了绘制或者滑动操作。
二、分析
工程中使用RecyclerView的场景是,有一个图片列表,图片有选中和未选中的状态。在每次点击图片的时候,判断是否选中然后调用notifyItemChanged方法更新UI状态。第一次点击的时候没有报错,点二次点击时就报错了。可以确定问题是因为调用notifyItemChanged方法导致的,接下来看具体日志找原因。
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.recyclerview.widget.RecyclerView{e51744b VFED..... ......ID 0,280-1440,2953 #7f09017f
app:id/rv_list}, adapter GalleryAdapter@71f8e28, layout:androidx.recyclerview.widget.GridLayoutManager@afe741, context:com.xx.xx.xx.activity.GalleryActivity@f0f49e5
at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:3051)
at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeChanged(RecyclerView.java:5547)
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeChanged(RecyclerView.java:12268)
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeChanged(RecyclerView.java:12258)
at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemChanged(RecyclerView.java:7370)
at com.xx.xx.xx.xx.GalleryAdapter$3.onCheckedChanged(GalleryAdapter.java:89)
at android.widget.CompoundButton.setChecked(CompoundButton.java:180)
at com.xx.xx.xx.xx.GalleryAdapter.onBindItem(GalleryAdapter.java:72)
at com.xx.xx.xx.xx.GalleryAdapter.onBindItem(GalleryAdapter.java:23)
at com.xx.common.base.adapter.BaseSingleBindingAdapter.onBindViewHolder(BaseSingleBindingAdapter.java:47)
at com.xx.common.base.adapter.BaseSingleBindingAdapter.onBindViewHolder(BaseSingleBindingAdapter.java:24)
at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7065)
at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7107)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6012)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6279)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
at androidx.recyclerview.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:561)
at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
at androidx.recyclerview.widget.GridLayoutManager.onLayoutChildren(GridLayoutManager.java:170)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1855)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at com.android.internal.policy.DecorView.onLayout(DecorView.java:786)
at android.view.View.layout(View.java:22152)
at android.view.ViewGroup.layout(ViewGroup.java:6290)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3338)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2815)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1935)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8055)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1227)
at android.view.Choreographer.doCallbacks(Choreographer.java:1029)
at android.view.Choreographer.doFrame(Choreographer.java:942)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1208)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7707)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
在RecyclerView中,当RecyclerView的Adapter更新数据时,按照流程会执行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--。也就是说上一次的绘制或者滑动还未结束,下一次的绘制或者滑动就开始了。对应到工程中的使用场景,上一次更新图片展示状态还没结束,就点击了图片开始下一次的状态更新。具体来说问题产生原因是是绘制更新导致的,不是滑动导致的。
三、方案
了解Android消息机制应该知道,Android中的UI的刷新其实是通过Handler消息机制实现的。通过对消息的分发,从而进行不同处理。从Log中最后几行也可以看出,最后几行对应的是调用notifyItemChanged方法后代码执行流程。
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
通过Handler.post方法将一个Runnable方法块放入MessageQueue中去等待执行。主线程的Looper会在循环取此队列中的消息。一般情况下,这个流程是串行的。等前面的消息处理完了再从MessageQueue中取剩下的处理。因此可以使用Handler.post方法将去执行notifyItemChanged操作,这样等前面的notifyItemChanged处理完后再执行后面的notifyItemChanged。解决方案如下:
mHandler.post(() -> notifyItemChanged(position));
另外根据源码可知还有一种情况会产生上面的问题,解决方案也很简单,进行一下判断处理是不是在滑动状态。如果是滑动状态就不要更新数据。
if (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE
&& (recyclerView.isComputingLayout() == false)) {
dataAdapter.notifyDataSetChanged();
}
四、总结
Handler是Android中非常重要的一个类,无论是在Framework层还是应用开发层都随处可见,尤其是和UI更新以及线程相关的方面。所以需要深入了解Handler的原理,这样对于与其相关问题可以很方便的解决。