您的位置:首页 > 其它

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

2016-04-19 15:21 591 查看
第一部分 crash描述及原因分析

 

 

在ListView和Adapter搭配使用时,有一个经典的安卓crash:Adapter数据源发生变化但是没有通知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进行布局的时候,在主要的逻辑开始之前会判断成员变量mItemCount与mAdapter当前的count是否相同。从这样的逻辑可以看出mItemCount显然是一个缓存变量,并不是mAdapter的当前值。所以先看看mItemCount是在哪些逻辑点被赋值。

 

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

ListView继承自AbsListView,AbsListView继承自AdapterView。

mItemCount在AdapterView中定义:

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

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

(1)ListView中,有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()供子类扩展。所以,从主线程看来,onMeasure和onLayout是连续的过程。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(),mItemCount与adapter
item count不相等,抛出exception。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: