Listview crash 分析

2016/4/11 posted in  Android

灰度的时候出的 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(2131624042, class android.widget.ListView) with Adapter(class com.sankuai.meituan.meituanwaimaibusiness.modules.order.adapter.j)]
    at android.widget.ListView.layoutChildren(ListView.java:1572)
    at android.widget.AbsListView.onLayout(AbsListView.java:2586)
    at android.view.View.layout(View.java:15752)
    at android.view.ViewGroup.layout(ViewGroup.java:4932)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1692)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1534)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1443)
    at android.view.View.layout(View.java:15752)
    at android.view.ViewGroup.layout(ViewGroup.java:4932)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1692)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1534)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1443)
    at android.view.View.layout(View.java:15752)
    at android.view.ViewGroup.layout(ViewGroup.java:4932)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    at android.view.View.layout(View.java:15752)
    at android.view.ViewGroup.layout(ViewGroup.java:4932)
    at com.android.internal.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:464)
    at android.view.View.layout(View.java:15752)
    at android.view.ViewGroup.layout(ViewGroup.java:4932)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    at android.view.View.layout(View.java:15752)
    at android.view.ViewGroup.layout(ViewGroup.java:4932)
    at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2410)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2125)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1292)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6678)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:813)
    at android.view.Choreographer.doCallbacks(Choreographer.java:613)
    at android.view.Choreographer.doFrame(Choreographer.java:583)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:799)
    at android.os.Handler.handleCallback(Handler.java:733)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:146)
    at android.app.ActivityThread.main(ActivityThread.java:5752)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
    at dalvik.system.NativeStart.main(Native Method)

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.

大概的意思是当 adapter 的数据改变时要及时调用notifyDataSetChanged(),并且是在 UI 线程进行的,检查了OrderAdpatersetData()调用处,均在 UI 线程

查看 ListView 源码,找到抛出异常的地方

// Handle the empty set by removing all views that are visible
// and calling it a day
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() + ")]");
}

可以看出 mItemCount != mAdapter.getCount()导致的。

mAdapter.getCount() 很好理解,就是我们复写Adapter 的 getCount()方法。

的调用处有2个地方

  1. public void setAdapter(ListAdapter adapter)
    mItemCount = mAdapter.getCount();

  2. onMeasure()
    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();

    所以有时没有调用notify并不会引起崩溃

  3. 还有一个地方比较隐蔽,在ListView的父类的父类AdapterView中,在notifyDataSetChanged()时调用

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect the case where a cursor that was previously invalidated has
            // been repopulated with new data.
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

所以崩溃的原因就是list 的数据变化时,没有及时notifyDataSetChanged()或者onMeasure()导致mItemCountgetCount()的数据不一致

在检查我们的adapter 的 setData() 方法

    public void setData(ArrayList<Order> data) {
        this.mOrderList = data;
        notifyDataSetChanged();
    }
    
        public void notifyDataSetChanged() {
        mHandler.removeMessages(MSG_NOTIFY_DATA_CHANGED);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_DATA_CHANGED));
    }
    
    public OrderAdapter(Context context, String page, boolean autoRefresh) {
        mContext = context;
        mOrderViewBinder = new OrderViewBinder(context, page);
        mAutoRefreshable = autoRefresh;
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                int what = msg.what;
                switch (what) {
                    case MSG_NOTIFY_DATA_CHANGED:
                        OrderAdapter.super.notifyDataSetChanged();

                        if (mAutoRefreshable) {
                            Message refreshMsg = mHandler.obtainMessage(MSG_NOTIFY_DATA_CHANGED);
                            mHandler.sendMessageDelayed(refreshMsg, REFRESH_FREQ);
                        }
                    default:
                        return;
                }
            }
        };
    }

在改变数据之后并没有及时的notifyDataSetChanged(),而是发了一个 message ,在 handler中在调用notifyDataSetChanged,在数据改变之后,可能 handler 还没收到消息,导致mItemCountgetCount()的数据不一致

解决办法

  1. handler中在对数据赋值并notify
  2. setdata之后立即notify