Android The content of the adapter has changed but ListView did not receive a notification问题分析

最近一同事的模块在monkey测试时报出了一个crash:

java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(2131427329, class android.widget.ListView) with Adapter ]
at android.widget.ListView.layoutChildren(ListView.java:1700)
at android.widget.AbsListView.onTouchModeChanged(AbsListView.java:3785)
at android.view.ViewTreeObserver.dispatchOnTouchModeChanged(ViewTreeObserver.java:1032)
at android.view.ViewRootImpl.ensureTouchModeLocally(ViewRootImpl.java:4059)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2163)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1403)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6877)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:966)
at android.view.Choreographer.doCallbacks(Choreographer.java:778)
at android.view.Choreographer.doFrame(Choreographer.java:713)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
at android.os.Handler.handleCallback(Handler.java:790)

1.  网上说的原因一般是修改adapter 数据和调用notifyDataSetChanged()方法不在同一线程,子线程修改adapter 数据后在主线程调用notifyDataSetChanged()方法引起概率性notifyDataSetChanged() 通知延时,造成crash:

大部分的时候我们是从网络上获取的数据,获取完成后,我们更改本地的数据源,然后调用notifyDataSetChanged()方法,
我们获取数据肯定不会在UI线程里面直接获取,一般来说,我们会新开一个线程,或者是用AsyncTask,还有就是一些第三方的框架,比如okhttp,我用的就是okhttp,不管使用哪种方法,新开一个线程是在run()方法里面,AsyncTask是在doInBackground()方法里面,okhttp是在onResponse()方法里面,我们获取的数据都是在子线程里面获取的,获取数据以后我们就直接更改了与Adapter绑定的数据集,这个数据集是在这个子线程里面更改的,但是我们不能再子线程里面调用notifyDataSetChanged()方法,我们必须在UI线程里面调用notifyDataSetChanged()方法,那么我们就要使用Handler或者runOnUiThread()方法来处理,这个时候问题来了,我们在子线程里面更改了数据集,但是在UI线程里面调用notifyDataSetChanged()方法,这是问题发生的原因之所在,这个异常不一定会时时出现,但是会偶尔出现,一旦出现不会提示报错在哪一行,全是内部的异常,而且如果你用了第三方的异常上报,也不会上报,非常麻烦,极难排查。 
解决的方案就是更改数据集和调用notifyDataSetChanged()方法一定要在UI线程,不能分开,而且要按顺序,先更改数据集,马上调用notifyDataSetChanged()方法。

2. 但是看了下同事的代码,自始至终都没有新起线程,更新adapter 数据和调用notifyDataSetChanged()方法都是在主线程中执行的。然后进一步看了下代码,发现一个更新adapter 数据的函数,这个函数会先清空list数据,然后会查询系统接口,在循环中add每个item,怀疑这个地方有问题。

我们先看下framework中这个异常的源码是咋写的:

 1 // Handle the empty set by removing all views that are visible
 2 // and calling it a day
 3 if (mItemCount == 0) {
 4     resetList();
 5     invokeOnItemScrollListener();
 6     return;
 7 } else if (mItemCount != mAdapter.getCount()) {
 8     throw new IllegalStateException("The content of the adapter has changed but "
 9             + "ListView did not receive a notification. Make sure the content of "
10             + "your adapter is not modified from a background thread, but only "
11             + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
12             + ") with Adapter(" + mAdapter.getClass() + ")]");
13 }

意思是指,当listView中缓存数据的个数和adapter.getCount()方法不一致时就会报错。

再分析下我们的代码,我们有一个更新adapter 数据的函数,这个函数会先清空list数据,然后会查询系统接口,在循环中add每个item,add完成后才调用notifyDataSetChanged()。如果ListView正好在这段add item的这段时间刷新view,就有可能引起问题。因此,这个地方我们可以new一个局部的List保存查找的结果,等整个查找完成后再将这个list拷贝到adapter用到的数据List上,这样就可以避免此问题了。

再总结下这种问题的解决方法:

1. 如果是在主线程更新list数据,并且更新list 数据能瞬间完成,更新list后要紧接调用notifyDataSetChanged()方法。

2.如果是在主线程更新list数据,但是更新list 数据耗时较长,可以先创建个数据对象来过渡,计算完成后拷贝到adapter的数据List上,然后调用notifyDataSetChanged()方法。

3.如果是在子线程更新list数据,修改数据时可以为adapter的数据源复制一个副本,在子线程中修改副本的数据即可,然后在主线程中将副本赋值给adapter数据源,然后调用notifyDataSetChanged()方法。这个和第2点实际上是一个意思。

猜你喜欢

转载自blog.csdn.net/ws6013480777777/article/details/84768524