天天看點

Adapter的The content of the adapter has changed問題分析

為了更好的了解這個問題出現的真正原因,建議首先看看下面兩篇文章:

淺析notifyDataSetChanged内部工作流程

ListView中requestLayout執行流程解析

綜合上面兩篇文章,我們可以把整個執行過程用下圖展示出來。這個圖非常的關鍵,一定要保證在看下文的時候已經了解了上圖的過程。

Adapter的The content of the adapter has changed問題分析

下面看看錯誤提示。

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中實作的,我們來跟跟源碼:

@Override
protected 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 = ; 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()方法。

@Override
protected 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 = ;
        int delta = ;

        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 >=  && 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 >=  && index < childCount) {
                oldSel = getChildAt(index);
            }

            // Remember the previous first child
            oldFirst = getChildAt();

            if (mNextSelectedPosition >= ) {
                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 == ) {
            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 = ; 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 - , childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = ;
            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 == ) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - , false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - , childrenBottom);
                }
            } else {
                if (mSelectedPosition >=  && 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(, 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 = ;
                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, , getChildCount() - );
                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 > ) {
            checkSelectionChanged();
        }

        invokeOnItemScrollListener();
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}
           

在上面代碼中,我們一眼就看出了報錯的地方:

if (mItemCount == ) {
        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工作原理完全解析,帶你從源碼的角度徹底了解

繼續閱讀