安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(一)

来源:互联网 发布:淘宝直播开通 编辑:程序博客网 时间:2024/05/12 06:52

第一部分 crash描述及原因分析

 

 

在ListViewAdapter搭配使用时,有一个经典的安卓crashAdapter数据源发生变化但是没有通知ListView

 

异常类型: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.

 

exception开始追踪研究安卓源代码(6.0.1_r10),探究一下上述crash发生的原因。

 

先看看ListView.java中在什么位置抛出这个exception

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


layoutChildren的时候,即对子View进行布局的时候,在主要的逻辑开始之前会判断成员变量mItemCountmAdapter当前的count是否相同。从这样的逻辑可以看出mItemCount显然是一个缓存变量,并不是mAdapter的当前值。所以先看看mItemCount是在哪些逻辑点被赋值。

 

mItemCount这个成员变量并非ListView定义,这里需要先看看ListView的类继承关系:

ListView继承自AbsListViewAbsListView继承自AdapterView

mItemCountAdapterView中定义:

    /**     * The number of items in the current adapter.     */    @ViewDebug.ExportedProperty(category = "list")    int mItemCount;

ListViewAbsListViewAdapterView三者位于同一package中(android.widget),并且有上述继承关系,所以mItemCount在三者的逻辑中共享。所以要想探究mItem在什么地方被赋值,需要查看这三个类:

1ListView中,有2处:

    @Override    public void setAdapter(ListAdapter adapter) {        ......        if (mAdapter != null) {            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();            mOldItemCount = mItemCount;            mItemCount = mAdapter.getCount();        ......        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        ......        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();        ......}

(2)AbsListView中,有2处:

   @Override    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);        if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {            if (!isAttachedToWindow() && mAdapter != null) {                // Data may have changed while we were detached and it's valid                // to change focus while detached. Refresh so we don't die.                mDataChanged = true;                mOldItemCount = mItemCount;                mItemCount = mAdapter.getCount();            }            resurrectSelection();        }}    @Override    protected void onAttachedToWindow() {        super.onAttachedToWindow();        final ViewTreeObserver treeObserver = getViewTreeObserver();        treeObserver.addOnTouchModeChangeListener(this);        if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {            treeObserver.addOnGlobalLayoutListener(this);        }        if (mAdapter != null && mDataSetObserver == null) {            mDataSetObserver = new AdapterDataSetObserver();            mAdapter.registerDataSetObserver(mDataSetObserver);            // Data may have changed while we were detached. Refresh.            mDataChanged = true;            mOldItemCount = mItemCount;            mItemCount = mAdapter.getCount();        }}

(3)AdapterView中,有2处,在内部类AdapterDataSetObserver.onChanged()onInvalidated()中:

class AdapterDataSetObserver extends DataSetObserver {        private Parcelable mInstanceState = null;        @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();        }        @Override        public void onInvalidated() {            mDataChanged = true;            if (AdapterView.this.getAdapter().hasStableIds()) {                // Remember the current state for the case where our hosting activity is being                // stopped and later restarted                mInstanceState = AdapterView.this.onSaveInstanceState();            }            // Data is invalid so we should reset our state            mOldItemCount = mItemCount;            mItemCount = 0;            mSelectedPosition = INVALID_POSITION;            mSelectedRowId = INVALID_ROW_ID;            mNextSelectedPosition = INVALID_POSITION;            mNextSelectedRowId = INVALID_ROW_ID;            mNeedSync = false;            checkFocus();            requestLayout();        }        public void clearSavedState() {            mInstanceState = null;        }}

逐一分析上面的六处。

setAdapter()不会被频繁使用,只在初始化Adapter时使用。

onMeasure()的时候能够及时的拿到Adapter最新的数据。众所周知,Android绘制View经历三个过程:measure()->layout()->draw()。对应着onMeasure()/onLayout()/onDraw()供子类扩展。所以,从主线程看来,onMeasureonLayout是连续的过程。ListView没有重写onLayout(),在父类AbsListView.onLayout()中,调用了layoutChildren()

onAttachedToWindow()onFocusChanged()调用时间不确定。

AdapterDataSetObserver.onInvalidated()用于处理错误逻辑。onChanged()不影响下面的结论,后文会专门分析。

 

所以,在ListView中,onMeasure中从mAdapter拿到count赋值给mItemCount这个全局变量,在onLayout中会不做更新继续使用,并且会检查这个是否与mAdapter当前的item数相等,如不相等,就会抛这个exception

 

我认为这样做的目的在于,绘制View是一个整体过程,要保证期间items/children views是稳定的,所以用一个只在onMeasure赋值一次不再改变的全局变量mItemCount贯穿,并且要在必要的时候校验,校验不符时抛出上述exception

 

至此,可以得出这个exception发生的原因:在UI线程绘制View的过程中(onMeasure()执行对mItemCount赋值后),工作线程有机会得以执行,并且update了数据,导致adapter item count发生变化,再回到UI线程,走到layoutChildren()mItemCountadapter item count不相等,抛出exception


0 0
原创粉丝点击