Listviewがデータを更新するときにクラッシュするThe content of the adapter has changed but did not receive a notification.

3506 ワード

説明:仕事で出会った問題を記録して、批判と指摘を歓迎します~
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、原因を分析する
なぜ?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はメインスレッドであるため、2つのスレッドの間に時間差があり、この時間差でリストをスライドするとlayoutChildren()が呼び出され、notifydatachangedはまだ実行されていないため、mItemCountとadapterである.getCount(データソース個数)が一致せずにクラッシュします.
6、解決方法
データソースとadapterを更新します.notifydatachangedは同じhandlerに置かれています.データソースの更新とadapterをタイムリーに実行します.notifydatachangedはいずれもプライマリスレッドであり、2つのhandlerでは行われないことを保証します.
参照先:
Listviewソース分析
http://blog.csdn.net/guolin_blog/article/details/44996879http://www.cnblogs.com/monodin/p/3874147.html