今天遇到以下異常:
java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's
contents without calling PagerAdapter#notifyDataSetChanged!
在描述問題解決之前,先說下項目列表顯示的機制吧
1、數據:
1)、Adapter接受到的是List,List容器中存放的是數據的實體類
2)、所有View存放在Map中,getCount()方法返回的是Map的size
2、視圖:
1)、Adapter首先會根據List的大小和展現的View,預加載,這里是每次下載48條數據,每頁12條,共4頁
2)、當ViewPager的滾動狀態為IDLE的情況下,會以當前頁為基准,向前創建一頁View,向后創建兩頁View
3)、所有View保存在Map中,當在調用instantiateItem方法的時候,直接從Map里邊取
3、更新:
1)、當數據下載完成,在主線程更改適配器中的List容器,並且調用notifyDataSetChanged();
2)、onPageSelected觸發會再次預加載的下一頁數據,更新完畢還會執行上一步
好,進入正文
很多帖子提到ADT更新到22之后,檢查更加嚴格,因此,每次數據更改都要調用notifyDataSetChanged方法,
我確實是這么做了,異步下載數據,下載完數據發送到主線程進行notifyDataSetChanged,結果,還是拋異常。
之前沒看過ViewPager源碼,這次就大概跟蹤下方法吧!
通過搜索ViewPager類,找到異常拋出位置,在populate方法中
1 final int N = mAdapter.getCount(); 2 // code here ... 3 if (N != mExpectedAdapterCount) { 4 String resName; 5 try { 6 resName = getResources().getResourceName(getId()); 7 } catch (Resources.NotFoundException e) { 8 resName = Integer.toHexString(getId()); 9 } 10 throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 11 " contents without calling PagerAdapter#notifyDataSetChanged!" + 12 " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 13 " Pager id: " + resName + 14 " Pager class: " + getClass() + 15 " Problematic adapter: " + mAdapter.getClass()); 16 }
關鍵就是mExpectedAdapterCount,那繼續找mExpectedAdapterCount的聲明和使用。
首先在setAdapter(PagerAdapter adapter)方法中找到賦值的地方,但是,不是設置適配器這個地方造成的異常,
所以,繼續查找。
最后查找到的只有在dataSetChanged()中再次使用過,代碼如下:
1 void dataSetChanged() { 2 // This method only gets called if our observer is attached, so mAdapter is non-null. 3 4 final int adapterCount = mAdapter.getCount(); 5 mExpectedAdapterCount = adapterCount; 6 // code here... 7 }
在PagerAdapter中調用notifyDataSetChanged()方法,數據更新的時候,mExpectedAdapterCount會被重新賦值
mExpectedAdapterCount和N不同,那只能查下dataSetChanged()和populate()調用的先后順序了
dataSetChanged()肯定是notifyDataSetChange()方法觸發,那就查找populate()
不說分析的過程了,直接上結果!如下
ViewPager每次翻頁方法執行順序:
dispatchKeyEvent->executeKeyEvent->arrowScroll->
pageLeft/pageRight->setCurrentItem->setCurrentItemInternal
在setCurrentItemInternal方法中,各種方法調用,會執行多次populate()方法,因此,會調用到多次getCount()
來獲取N的值,如下圖

問題出來了,當翻頁的時候,populate()方法會調用多次,直到狀態為IDLE的時候,會創建預加載的一頁視圖,
此時,Adapter中存放View的Map會增加,getCount返回值變大。
這時候數據並未下載下來,那並不會notifyDataSetChanged()方法,mExpectedAdapterCount的值還是上次的值
因此,如下條件成立,進入代碼,拋出異常
1 if (N != mExpectedAdapterCount) { 2 // code here... 3 }
如下圖(最后一條Log為5,其實之后還會打印多次只是這時已經在populate()方法除拋異常,不會再繼續執行):

==================================================================
仔細思考思考,其實是對notifyDataSetChanged()方法的調用時機有誤解,並不是適配器數據更新的時候調用,
而是在getCount()發生改變的時候去調用,哪里影響了getCount(),就應該再哪里調用!
因此,在列表機制的第2-2步中去調用notifyDataSetChanged()方法就解決問題了!
記錄下來,加深印象!
