安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(一)
来源:互联网 发布:淘宝直播开通 编辑:程序博客网 时间:2024/05/12 06:52
第一部分 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。
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(一)
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(二)
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(三)
- 安卓学习笔记(一)——线程的用法及怎样在子线程中更新UI
- adapter数据更新要放在ui线程中
- 安卓自定义Adapter,以及如何提高ListView的效率
- 【Android笔记-异常-4】定义一个临时的数组变量承接数据,ListView的数据以及通知数据更新要放到同一个线程(主线程)。避免出现异常"The content of the adapter
- 安卓listview adapter
- 安卓打造listview的万用adapter
- android 在其他线程中更新UI线程的解决方法
- Android 在其他线程中更新UI线程的解决方法
- Android 在其他线程中更新UI线程的解决方法
- Android 在其他线程中更新UI线程的解决方法
- Android开发中ListView数据更新显示的解决方法
- ListView中使用自定义Adapter及时更新数据
- 关于ListView中使用自定义Adapter及时更新数据
- Android 中listView数据混乱的原因以及解决方法
- 记录dispatch_semaphore_t在ARC中引发的一起crash事故
- Play框架内置模板标签
- wpa_supplicant.conf文件详解
- MySQL数据导入Redis
- AndroidUI之三分钟教你实现效果简单大气的Dialog提示框
- java 根据OU获取Windows的AD域账户
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(一)
- JS调用OC简单实例-part1
- 总结一哈JDK和Tomcat的环境变量配置
- javase面试要点
- bzoj3956 Count 解题报告
- 我的25年嵌入式生涯(转载)
- PHP中require和include路径问题总结
- JFreeChart中文显示乱码,英文正常的solution
- Tcl学习 - 第一天