安卓探究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:
在layoutChildren的时候,即对子View进行布局的时候,在主要的逻辑开始之前会判断成员变量mItemCount与mAdapter当前的count是否相同。从这样的逻辑可以看出mItemCount显然是一个缓存变量,并不是mAdapter的当前值。所以先看看mItemCount是在哪些逻辑点被赋值。
mItemCount这个成员变量并非ListView定义,这里需要先看看ListView的类继承关系:
ListView继承自AbsListView,AbsListView继承自AdapterView。
mItemCount在AdapterView中定义:
ListView、AbsListView、AdapterView三者位于同一package中(android.widget),并且有上述继承关系,所以mItemCount在三者的逻辑中共享。所以要想探究mItem在什么地方被赋值,需要查看这三个类:
(1)ListView中,有2处:
(2)AbsListView中,有2处:
(3)AdapterView中,有2处,在内部类AdapterDataSetObserver.onChanged()和onInvalidated()中:
逐一分析上面的六处。
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。
在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。
相关文章推荐
- mongodb.properties文件
- 使用mitmf 来绕过HSTS站点抓取登陆明文
- openURL in APP Extension
- java 根据OU获取Windows的AD域账户
- maven导出项目依赖的jar包
- Javadoc使用
- @Inject
- 大数据技能图谱
- installshield生成时提示6003错误的一种可能
- 图片放大效果
- 让一元素吸底
- C# 使用FileStream文件流对文件进行读取写入
- 安卓消息处理机制
- MySQL中的where语句
- Problem B
- Problem B
- JavaScript canvas 绘制圆形时钟
- 【OpenCV】OpenCV3的第十天——imgproc组件之目标检测
- Oracle 查看表对应注释
- synchronized run()方法