Android RecyclerView is computing a layout or scrolling IllegalStateException

8,542 阅读3分钟

一、问题

在使用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的原理,这样对于与其相关问题可以很方便的解决。