1. 程式人生 > >Listview更新資料時崩潰The content of the adapter has changed but did not receive a notification.

Listview更新資料時崩潰The content of the adapter has changed but did not receive a notification.

說明:在工作中遇到的問題記錄下來,歡迎批評和指正~

1、問題

在listView上下拉重新整理或者滑動過程中經常碰到這個復現率比較高的崩潰問題 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、崩潰解釋

是說資料來源更新了,但是listview沒有及時收到通知進行更新。確保adapter內容更新是在主執行緒。

3、崩潰場景

1、請求資料是在子執行緒中進行,之後在該執行緒中對資料來源dataList進行更新; 2、通過handler.sendmessage發訊息到主執行緒notifydatachange。【或者資料來源更新和notifydatachange都在主執行緒但不在同一個handler】

4、異常在程式碼中的體現

在ListView.java layoutChildren()方法中,如下。當mItenCount != mAdapter.getCount時就會異常。
protected void layoutChildren() {
......
if (mItemCount == 0) {
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new 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(" + getId() + ", " + getClass()
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }
......
}

5、分析原因

為什麼mItemCount和adapter.getCount()會不相等?mItemCount什麼時候會被賦值,layoutChilren()什麼時候會被執行呢?   mItemCount賦值時機: 1、ListView.setAdapter方法中有mItemCount = adapte.getCount()   2、ListView onMeasure中也存在mItemCount = adapte.getCount(),即重繪會重新賦值,例如:notifydatachanged之後會進行重繪。列表滑動結束action_up之後也會重繪。   layoutChidren()的呼叫時機: 
1、ListView每次重繪onLayout()方法中會呼叫layoutChidren()。比如notyfydatachanged之後都會重新requestlayout。從而呼叫layoutChildren()   2、手勢滑動 case MotionEvent.ACTION_MOVE: 中呼叫。
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    if (mHasPerformedLongPress) {
        // Consume all move events following a successful long press.
        return;
    }

    int pointerIndex = ev.findPointerIndex(mActivePointerId);
    if (pointerIndex == -1) {
        pointerIndex = 0;
        mActivePointerId = ev.getPointerId(pointerIndex);
    }

    if (mDataChanged) {//資料來源頭更新 mDataChanged會為true
        // Re-sync everything if data has been changed
        // since the scroll operation can query the adapter.
        layoutChildren();
    }
......
那麼問題就在於在子執行緒更新資料來源之後(mDataChanged會置為true),由於notifydatachanged在主執行緒,兩個執行緒之間是存在時間差的,在這個時間差額中如果滑動列表就導致layoutChildren()被呼叫,而notifydatachanged還未執行,所以mItemCount和adapter.getCount(資料來源個數)不一致而崩潰。

6、解決辦法

把資料來源更新和adapter.notifydatachanged放在同一個handler中。及時資料來源更新和adapter.notifydatachanged都是在主執行緒也要保證不能是在兩個handler中進行。

參考:

listview原始碼分析