Adapter的The content of the adapter has changed问题分析
来源:互联网 发布:程序员的自我修养txt 编辑:程序博客网 时间:2024/06/07 23:47
为了更好的理解这个问题出现的真正原因,建议首先看看下面两篇文章:
浅析notifyDataSetChanged内部工作流程
ListView中requestLayout执行流程解析
综合上面两篇文章,我们可以把整个执行过程用下图展示出来。这个图非常的关键,一定要保证在看下文的时候已经理解了上图的过程。
下面看看错误提示。
07-28 17:22:02.162: E/AndroidRuntime(16779): java.lang.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(2131034604, class android.widget.ListView) with Adapter(class com.nodin.sarah.HeartListAdapter)]
这个问题就是当我们调用notifyDataSetChanged()的过程中,数据源可能发生了改变,这样出现前后数据源不一致而报错,可能这样说还是不好理解,下面我们可以从源码的角度来进行分析。
从浅析notifyDataSetChanged内部工作流程文章中我们需要理出以下两点:
当执行notifyDataSetChanged(),它内部做了两件事情,具体的代码在AdapterDataSetObserver的onChanged方法里面:
1、mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
2、requestLayout();
可以看到做完这两件事情后,它进入了requestLayout()方法中,requestLayout方法中做了什么事情,这里就不细说,看看ListView中requestLayout执行流程解析这篇文章应该就知道了,从上图可以知道,requestLayout内部其实就是执行了performTraversals(),performTraversals()内部执行的就是measure,layout,draw方法,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的功能其实都是在onLayout()方法中进行的了,所以我重点关注到onLayout()方法,从图知道它执行了layoutChildren()方法。
ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView中实现的,我们来跟跟源码:
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mInLayout = true; if (changed) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } if (mFastScroller != null && (mItemCount != mOldItemCount || mDataChanged)) { mFastScroller.onItemCountChanged(mItemCount); } //这个方法是重点 layoutChildren(); mInLayout = false; mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;}
onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。重点调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法。
@Overrideprotected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (blockLayoutRequests) { return; } mBlockLayoutRequests = true; try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } final int childrenTop = mListPadding.top; final int childrenBottom = mBottom - mTop - mListPadding.bottom; final int childCount = getChildCount(); int index = 0; int delta = 0; View sel; View oldSel = null; View oldFirst = null; View newSel = null; // Remember stuff we will need down below switch (mLayoutMode) { case LAYOUT_SET_SELECTION: index = mNextSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { newSel = getChildAt(index); } break; case LAYOUT_FORCE_TOP: case LAYOUT_FORCE_BOTTOM: case LAYOUT_SPECIFIC: case LAYOUT_SYNC: break; case LAYOUT_MOVE_SELECTION: default: // Remember the previously selected view index = mSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { oldSel = getChildAt(index); } // Remember the previous first child oldFirst = getChildAt(0); if (mNextSelectedPosition >= 0) { delta = mNextSelectedPosition - mSelectedPosition; } // Caution: newSel might be null newSel = getChildAt(index + delta); } boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } // 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() + ")]"); } setSelectedPositionInt(mNextSelectedPosition); // Remember which child, if any, had accessibility focus. final int accessibilityFocusPosition; final View accessFocusedChild = getAccessibilityFocusedChild(); if (accessFocusedChild != null) { accessibilityFocusPosition = getPositionForView(accessFocusedChild); accessFocusedChild.setHasTransientState(true); } else { accessibilityFocusPosition = INVALID_POSITION; } // Ensure the child containing focus, if any, has transient state. // If the list data hasn't changed, or if the adapter has stable // IDs, this will maintain focus. final View focusedChild = getFocusedChild(); if (focusedChild != null) { focusedChild.setHasTransientState(true); } // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillFromMiddle(childrenTop, childrenBottom); } break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; default: if (childCount == 0) { if (!mStackFromBottom) { final int position = lookForSelectablePosition(0, true); setSelectedPositionInt(position); sel = fillFromTop(childrenTop); } else { final int position = lookForSelectablePosition(mItemCount - 1, false); setSelectedPositionInt(position); sel = fillUp(mItemCount - 1, childrenBottom); } } else { if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); } else { sel = fillSpecific(0, childrenTop); } } break; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); if (sel != null) { final boolean shouldPlaceFocus = mItemsCanFocus && hasFocus(); final boolean maintainedFocus = focusedChild != null && focusedChild.hasFocus(); if (shouldPlaceFocus && !maintainedFocus && !sel.hasFocus()) { if (sel.requestFocus()) { // Successfully placed focus, clear selection. sel.setSelected(false); mSelectorRect.setEmpty(); } else { // Failed to place focus, clear current (invalid) focus. final View focused = getFocusedChild(); if (focused != null) { focused.clearFocus(); } positionSelector(INVALID_POSITION, sel); } } else { positionSelector(INVALID_POSITION, sel); } mSelectedTop = sel.getTop(); } else { // If the user's finger is down, select the motion position. // Otherwise, clear selection. if (mTouchMode == TOUCH_MODE_TAP || mTouchMode == TOUCH_MODE_DONE_WAITING) { final View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) { positionSelector(mMotionPosition, child); } } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } } if (accessFocusedChild != null) { accessFocusedChild.setHasTransientState(false); // If we failed to maintain accessibility focus on the previous // view, attempt to restore it to the previous position. if (!accessFocusedChild.isAccessibilityFocused() && accessibilityFocusPosition != INVALID_POSITION) { // Bound the position within the visible children. final int position = MathUtils.constrain( accessibilityFocusPosition - mFirstPosition, 0, getChildCount() - 1); final View restoreView = getChildAt(position); if (restoreView != null) { restoreView.requestAccessibilityFocus(); } } } if (focusedChild != null) { focusedChild.setHasTransientState(false); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; if (mPositionScrollAfterLayout != null) { post(mPositionScrollAfterLayout); mPositionScrollAfterLayout = null; } mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } }}
在上面代码中,我们一眼就看出了报错的地方:
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() + ")]"); }
原来错误是从这里报处理的,那什么时候会报错呢?我们看看上面的判断条件:
mItemCount != mAdapter.getCount()。
在上面我们说过notifyDataSetChanged干了两件事,其中的第一件事就是给mItemCount赋值。可以回过头去看看mItemCount = getAdapter().getCount();
现在我们应该已经知道了,当我们调用了notifyDataSetChanged之后,说明数据源已经发生了变化,所以它会重新获取到Adapter里面的count赋值给mItemCount,接着就是执行重新布局,如果我们在给mItemCount赋值之后到执行上面的这个判断之间再一次修改了数据源,那么当执行到上面的这个判断的时候,就会出现mAdapter.getCount()获取的count数是新的数据源的count,跟之前存取mItemCount不一致,这样就会抛出这个异常。也就是说当我们mItemCount赋值之后,如果在执行if (mItemCount != mAdapter.getCount())之前又修改了数据源,就会出现异常。
这个异常一般出现的具体场合,参考下面文章:
关于Adapter的The content of the adapter has changed问题分析
ListView/Adapter IllegalStateException
参考文章:
Android ListView工作原理完全解析,带你从源码的角度彻底理解
- 关于Adapter的The content of the adapter has changed问题分析
- 关于Adapter的The content of the adapter has changed问题分析
- Android开发教程--关于Adapter的The content of the adapter has changed问题分析
- Adapter的The content of the adapter has changed问题分析
- 关于Adapter的The content of the adapter has changed问题分析
- 关于Adapter的The content of the adapter has changed问题分析
- 关于Adapter的The content of the adapter has changed问题分析
- 关于Adapter的The content of the adapter has changed问题分析
- 关于Adapter的The content of the adapter has changed but ListView did not receive a notification.问题分析
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- Adapter报错:The content of the adapter has changed
- leetcode 139 —— Word Break
- centos6.7源码安装mysql
- 小白学开发(iOS)OC_ 字符串重组(2015-08-13)
- OC - NSSetAndNSMutableSet
- 二分图学习小结
- Adapter的The content of the adapter has changed问题分析
- 注册表常识1
- 策略模式
- 小白学开发(iOS)OC_ 字符串写入文件(2015-08-13)
- 设计模式之代理模式
- jeecms标签对应的类,方便查找标签的输入输出参数,以及参数对应的数值类型、范围
- 关于Qt中自定义槽和信号的简单示例
- 小白学开发(iOS)OC_ 从文件中读取字符串内容(2015-08-14)
- XSS教程