用户滚动ListView时更新数据,可能引发异常

71 阅读1分钟

在用户滚动时更新了ListView数据,可能导致在layoutChildren()中因为itemCount与adapter.count()不一致,产生IllegalStateException。


protected void layoutChildren() {
    ...
    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() + ")]");
}
    ...

}

原因:

  1. notifyDataChanged()的调用链为:

    notifyDataChanged() 
        -> AdapterDataSetObserver.onChanged() 
            -> ListView.requestLayout()
    

    布局任务会被放到消息队列中,等待vsync信号到来时执行。在执行ListView.onMeasure()时,会更新itemCount的数值为adapter.count()。

        
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        ...
    }
    
  2. 用户上下滚动ListView,产生的输入事件也被放到消息队列中,等待vsync信号到来时执行。这些输入事件会最终引起ListView.layoutChildren()被调用。

  3. Choreographer 执行顺序:

    1. 输入事件处理(ConsumeBatchedInputRunnable)。
    2. 动画处理。
    3. 布局与绘制(performTraversals())。
  4. 在vsync信号到来时输入事件是早于布局任务被执行的,在数据更新之后,出现没有调用onMeasure()就执行layoutChildren()的情况,从而出现itemCount与adapter.count()的数值不一致。