關於Adapter的The content of the adapter has changed問題分析
1、問題描述
1 07-28 17:22:02.162: E/AndroidRuntime(16779): 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(2131034604, class android.widget.ListView) with Adapter(class com.nodin.sarah.HeartListAdapter)] 2 3 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.ListView.layoutChildren(ListView.java:1555) 4 5 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.AbsListView.onLayout(AbsListView.java:2091) 6 7 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 8 9 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 10 11 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1055) 12 13 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 14 15 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 16 17 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453) 18 19 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.FrameLayout.onLayout(FrameLayout.java:388) 20 21 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 22 23 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 24 25 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1589) 26 27 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 28 29 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 30 31 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1055) 32 33 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 34 35 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 36 37 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453) 38 39 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.FrameLayout.onLayout(FrameLayout.java:388) 40 41 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 42 43 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 44 45 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671) 46 47 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525) 48 49 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.LinearLayout.onLayout(LinearLayout.java:1434) 50 51 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 52 53 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 54 55 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453) 56 57 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.widget.FrameLayout.onLayout(FrameLayout.java:388) 58 59 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.View.layout(View.java:14785) 60 61 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewGroup.layout(ViewGroup.java:4631) 62 63 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:1985) 64 65 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1742) 66 67 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:998) 68 69 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5582) 70 71 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749) 72 73 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.Choreographer.doCallbacks(Choreographer.java:562) 74 75 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.Choreographer.doFrame(Choreographer.java:532) 76 77 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735) 78 79 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.os.Handler.handleCallback(Handler.java:733) 80 81 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.os.Handler.dispatchMessage(Handler.java:95) 82 83 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.os.Looper.loop(Looper.java:137) 84 85 07-28 17:22:02.162: E/AndroidRuntime(16779): at android.app.ActivityThread.main(ActivityThread.java:4998) 86 87 07-28 17:22:02.162: E/AndroidRuntime(16779): at java.lang.reflect.Method.invokeNative(Native Method) 88 89 07-28 17:22:02.162: E/AndroidRuntime(16779): at java.lang.reflect.Method.invoke(Method.java:515) 90 91 07-28 17:22:02.162: E/AndroidRuntime(16779): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777) 92 93 07-28 17:22:02.162: E/AndroidRuntime(16779): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593) 94 95 07-28 17:22:02.162: E/AndroidRuntime(16779): at dalvik.system.NativeStart.main(Native Method) 96 97 07-28 17:22:02.162: W/ActivityManager(588): Force finishing activity
2、復現場景
使用ListView和Adapter實現動態增刪數據列表功能,初始化數據分為兩部分:本地和網絡。所以在Adapter的數據初始化的時候,先講本地數據添加到了容器內。同時發起網絡請求,等加載完畢后追加到容器內。
問題出現在:當網絡請求完畢后追加數據的時候,拋出上述異常。
3、原因分析
Exception解讀:
Adapter的數據內容已經改變,但是ListView卻未接收到通知。要確保不在后台線程中修改Adapter的數據內容,而要在UI Thread中修改。確保Adapter的數據內容改變時一定要調用notifyDataSetChanged()方法。
且不管Exception內容,先查詢Android源碼看看該Exception是從哪里拋出來的。
在ListView的layoutChildren()方法里有如下一段方法:
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緩存的數據Count和ListView中Adapter.getCount()不等時,會拋出該異常。
結合開頭的異常解讀,可以斷定肯定是Adapter數據動態更新的問題。仔細檢查了自己的代碼:
當網絡請求完畢后,直接在網絡線程(非UI線程)里調用了在Adapter中新增的自定義方法addData(List)更新數據,而addData(List)方法內更新換完數據后,通過Handler發送Message的策略調用Adapter的notifyDataSetChanged()方法通知更新。
這么一來,並不能保證Adapter的數據更新時,立馬調用notifyDataSetChanged()通知ListView,這兩個線程之間的時間差引起的數據不同步,導致ListView的layoutChildren()中訪問Adapter的getCount()方法時,Adapter內已經是最新數據源,而ListView內的緩存數據Count仍是舊數據的Count,該問題最終原因終於浮出水面。
4、解決方案
在本例中,解決方案是:
把addData(List)方法內更新數據的代碼挪出來,和notifyDataSetChanged()方法一同放在Handler里,保證數據更新時及時通知ListView。
為了盡量避免該問題,以后編程盡量從如下幾個方面檢查自己的代碼:
- 確保Adapter的數據更新后一定要調用notifyDataSetChanged()方法通知ListView
- 數據更新和notifyDataSetChanged()放在UI線程內,且必須同步順序執行,不可異步
- 仔細檢查確認getCount()方法返回值是否正確