跳转至

ListView、RecyclerView缓存策略解析

RecyclerView高级特性系列:


本文源码版本为27

本章的主要内容为ListView、RecyclerView缓存策略解析。虽然ListView现在基本被更强大的RecyclerView所取代,但我们还是要了解一下其缓存原理,吸收一下思想。

所以,先从简单一点的ListView开始。

1. ListView缓存策略

ListView和GridView一样,都继承自AbsListView。而缓存策略的实现是在AbsListView类中,所以两者的缓存策略也是一致的。

我们体验ListView的缓存最多的地方就是BaseAdapter.getView(int, View, ViewGroup)方法中了,一个典型的BaseAdapter的实现如下:

public class DemoAdapter extends BaseAdapter {
    private Context mContext;
    private List<Data> mDataList;

    public DemoAdapter(Context context, List<Data> dataList) {
        mContext = context;
        mDataList = dataList;
    }

    @Override
    public int getCount() {
        return mDataList.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.item_location_hot_city, null);
            viewHolder = new ViewHolder(convertView);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        final Data data = mDataList.get(position);
        viewHolder.tvTitle.setText(data.getName());

        return convertView;
    }

    private static class ViewHolder {
        View itemView;
        TextView tvTitle;

        public ViewHolder(View view) {
            itemView = view;
            tvTitle = (TextView) view.findViewById(R.id.tv_title);
        }
    }
}

在上面的方法中,一个非常重要的优化点就是getView方法中对convertView的判断。如果为空就需要我们创建一下;如果不为空,就说明是 缓存 的View,可以直接拿来填充数据。

那么,ListView如何管理缓存的View,什么时候调用BaseAdapter.getView方法并传入缓存的View或者null呢。这就是本节的重点,本节的讨论都是体现在convertView上。

1.1 RecycleBin

在讲AbsListViewListView代码之前,先说一下AbsListView.RecycleBin,该类负责管理view的复用。RecycleBin 有两个等级的缓存:ActiveViewsScrapViews

  • ActiveViews是指显示在屏幕上的view
  • ScrapViews是可能被adapter重新使用的老view,这样可以避免不必要的view创建。

RecycleBin里面的字段如下:

/**
  * The position of the first view stored in mActiveViews.
  */
private int mFirstActivePosition;

/**
  * Views that were on screen at the start of layout. This array is populated at the start of
  * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
  * Views in mActiveViews represent a contiguous range of Views, with position of the first
  * view store in mFirstActivePosition.
  */
private View[] mActiveViews = new View[0];

/**
  * Unsorted views that can be used by the adapter as a convert view.
  */
private ArrayList<View>[] mScrapViews;

private int mViewTypeCount;

private ArrayList<View> mCurrentScrap;

private ArrayList<View> mSkippedScrap;

private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;

解释如下:

  • mFirstActivePositionmActiveViews
    mFirstActivePosition是指mActiveViews中第一个View在ListView中的position
    mActiveViews是指正在屏幕上显示的View;在layout发生前保存,供layout中进行复用,在layout后会将剩余没有复用的View降级到scrap中

  • mScrapViewsmViewTypeCountmCurrentScrap
    可以被Adapter作为convert view使用的View
    mScrapViews根据mViewTypeCount的值来确定有数组都多大,无论数组具体多大,mCurrentScrap = mScrapViews[0]都成立。但一般来说

    • mViewTypeCount为1时,ScrapViews就是指mCurrentScrap
    • mViewTypeCount大于1时,ScrapViewsmScrapViews
  • mTransientStateViewsByIdmTransientStateViewsmSkippedScrap
    View.hasTransientState()为true时,会使用这上面的数据结构存储ScrapViews
    Adapter.hasStableIds()为true时,使用mTransientStateViewsById存储
    mDataChanged为false时,使用mTransientStateViews存储
    否则,使用mSkippedScrap存储,该List里面的View稍后会被清除,不会被复用

RecycleBin的方法本质上就是对上面数据的一些操作。主要的方法有:

  • setViewTypeCount(int)
    根据传入值为每个类型的数据元都申请一个List来存放缓存
  • fillActiveViews(int childCount, int firstActivePosition)
    保存firstActivePosition的值,并将[0, childCount)范围的View保存到mActiveViews数组中
  • getActiveView(int)
    与上面过程相反,首先将传入参数减去firstActivePosition得到View在mActiveViews数组中的下标,然后用下标取View
  • addScrapView(View, int)
    将旧View保存到对应的ScrapViewsmTransientStateViewsByIdmTransientStateViewsmSkippedScrap集合中
  • getScrapView(int)
    ScrapViews集合中获取旧View
  • getTransientStateView(int)
    mTransientStateViewsByIdmTransientStateViews集合中获取旧View

在了解了RecycleBin的重要字段和方法之后,下面可以开始分析ListView的缓存机制了。我们以ListView初次layout、再次layout以及用户滑动三个过程来分析。

1.2 ListView初次layout

ListView初次layout,显然是没有任何子view以及缓存的view的,我们看看这种情况下ListView的流程。首先,onMeasure方法显然是不需要分析的,因为不涉及到缓存的设计。所以,我们直接看基类AbsListViewonLayout方法。

/**
  * Subclasses should NOT override this method but
  *  {@link #layoutChildren()} instead.
  */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    mInLayout = true;

    final int childCount = getChildCount();
    if (changed) {
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
        mRecycler.markChildrenDirty();
    }

    layoutChildren();

    mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

    // TODO: Move somewhere sane. This doesn't belong in onLayout().
    if (mFastScroll != null) {
        mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
    }
    mInLayout = false;
}

这里第12行的判断中,changed是否为true都不重要,因为没有任何子view。然后19行layoutChildren()方法是一个要点,AbsListView并没有实现该方法,而是交给了子类来实现。所以,我们看看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 = 0;
        int delta = 0;

        View sel;
        View oldSel = null;
        View oldFirst = null;
        View newSel = null;

        ...

        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);

        ...

        // 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_MOVE_SELECTION:
            ...
        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();

        // remove any header/footer that has been temp detached and not re-attached
        removeUnusedFixedViews(mHeaderViewInfos);
        removeUnusedFixedViews(mFooterViewInfos);

        ...

        // Tell focus view we are done mucking with it, if it is still in
        // our view hierarchy.
        if (focusLayoutRestoreView != null
                && focusLayoutRestoreView.getWindowToken() != null) {
            focusLayoutRestoreView.dispatchFinishTemporaryDetach();
        }

        mLayoutMode = LAYOUT_NORMAL;
        mDataChanged = false;
        if (mPositionScrollAfterLayout != null) {
            post(mPositionScrollAfterLayout);
            mPositionScrollAfterLayout = null;
        }
        mNeedSync = false;
        setNextSelectedPositionInt(mSelectedPosition);

        updateScrollIndicators();

        if (mItemCount > 0) {
            checkSelectionChanged();
        }

        invokeOnItemScrollListener();
    } finally {
        if (mFocusSelector != null) {
            mFocusSelector.onLayoutComplete();
        }
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

在上面的代码中,可以直奔重点,直接来到第63的if语句。dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,显然此时为false。所以会执行第68行的recycleBin.fillActiveViews(childCount, firstPosition)将ListView中的View进行缓存,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。

接下来,会来到第75行的switch语句,一般情况下ListView的 layout mode 为LAYOUT_NORMAL,所以会走default分支。由于childCount目前为0,且mStackFromBottom默认为false,表示默认从上往下进行布局,所以会执行第84行的fillFromTop()方法。

fillFromTop(int)会调用fillDown(int, int)方法从上到下填充ListView,直到加载完了一屏数据或者数据加载完毕:

/**
  * Fills the list from top to bottom, starting with mFirstPosition
  *
  * @param nextTop The location where the top of the first item should be
  *        drawn
  *
  * @return The view that is currently selected
  */
private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    return fillDown(mFirstPosition, nextTop);
}

/**
  * Fills the list from pos down to the end of the list view.
  *
  * @param pos The first position to put in the list
  *
  * @param nextTop The location where the top of the item associated with pos
  *        should be drawn
  *
  * @return The view that is currently selected, if it happens to be in the
  *         range that we draw.
  */
private View fillDown(int pos, int nextTop) {
    View selectedView = null;

    int end = (mBottom - mTop);
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end -= mListPadding.bottom;
    }

    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }

    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
    return selectedView;
}

在上面第37行的while循环条件中,前面的nextTop < end判断的是是否超过了屏幕,每次添加新view之后nextTop值都会累加,后面的pos < mItemCount判断的是是否所有数据已经显示完毕。
第40行的makeAndAddView应该是fill过程的重点,我们看看如何make、如何add view的。

/**
  * Obtains the view and adds it to our list of children. The view can be
  * made fresh, converted from an unused view, or used as is if it was in
  * the recycle bin.
  *
  * @param position logical position in the list
  * @param y top or bottom edge of the view to add
  * @param flow {@code true} to align top edge to y, {@code false} to align
  *             bottom edge to y
  * @param childrenLeft left edge where children should be positioned
  * @param selected {@code true} if the position is selected, {@code false}
  *                 otherwise
  * @return the view that was added
  */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

mDataChanged显然还是false的,因此会尝试调用RecycleBin.getActiveView获取一个layout开始时fill的view(上面ListView.layoutChildren()方法的第68行recycleBin.fillActiveViews(childCount, firstPosition)会fill view到ActiveViews中),显然此时获取不到view。所以,接下来会执行第30行的obtainView方法创建或复用View,然后接着调用行第32行的setupChild方法放置并测量View。
那么obtainView()内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:

/**
 * Gets a view and have it show the data associated with the specified
 * position. This is called when we have already discovered that the view
 * is not available for reuse in the recycle bin. The only choices left are
 * converting an old view or making a new one.
 *
 * @param position the position to display
 * @param outMetadata an array of at least 1 boolean where the first entry
 *                    will be set {@code true} if the view is currently
 *                    attached to the window, {@code false} otherwise (e.g.
 *                    newly-inflated or remained scrap for multiple layout
 *                    passes)
 *
 * @return A view displaying the data associated with the specified position
 */
View obtainView(int position, boolean[] outMetadata) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

    outMetadata[0] = false;

    // Check whether we have a transient state view. Attempt to re-bind the
    // data and discard the view if we fail.
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

        // If the view type hasn't changed, attempt to re-bind the data.
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);

            // If we failed to re-bind the data, scrap the obtained view.
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        outMetadata[0] = true;

        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }

    if (mCacheColorHint != 0) {
        child.setDrawingCacheBackgroundColor(mCacheColorHint);
    }

    ...

    setItemViewLayoutParams(child, position);

    ...

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);

    return child;
}

obtainView()方法中代码包含了非常重要的逻辑,整个ListView最重要的内容就在这里。首先,会设置outMetadata[0]为false,这里的outMetadata实际上就是mIsScrap变量,该变量后面会用到。然后调用RecycleBin.getTransientStateView方法获取transient状态的scrap view。显然,目前没有这样的view。接着会到第45行执行RecycleBin.getScapView方法获取一个scrap view,显然,目前还是没有这样的view,所以scrapView为null。最后,会调用Adapter.getView方法来获取一个view。
回想一下文章开头在ListView的缓存策略写的一个典型的BaseAdapter的实现,如果传入的convertView为null,我们就会inflate一个view并返回。返回的view也会作为obtainView的结果进行返回,最终传入setupChild中:

/**
  * Adds a view as a child and make sure it is measured (if necessary) and
  * positioned properly.
  *
  * @param child the view to add
  * @param position the position of this child
  * @param y the y position relative to which this view will be positioned
  * @param flowDown {@code true} to align top edge to y, {@code false} to
  *                 align bottom edge to y
  * @param childrenLeft left edge where children should be positioned
  * @param selected {@code true} if the position is selected, {@code false}
  *                 otherwise
  * @param isAttachedToWindow {@code true} if the view is already attached
  *                           to the window, e.g. whether it was reused, or
  *                           {@code false} otherwise
  */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

    final boolean isSelected = selected && shouldShowSelector();
    final boolean updateChildSelected = isSelected != child.isSelected();
    final int mode = mTouchMode;
    final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
            && mMotionPosition == position;
    final boolean updateChildPressed = isPressed != child.isPressed();
    final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
            || child.isLayoutRequested();

    // Respect layout params that are already in the view. Otherwise make
    // some up...
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    }
    p.viewType = mAdapter.getItemViewType(position);
    p.isEnabled = mAdapter.isEnabled(position);

    // Set up view state before attaching the view, since we may need to
    // rely on the jumpDrawablesToCurrentState() call that occurs as part
    // of view attachment.
    if (updateChildSelected) {
        child.setSelected(isSelected);
    }

    if (updateChildPressed) {
        child.setPressed(isPressed);
    }

    if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
        if (child instanceof Checkable) {
            ((Checkable) child).setChecked(mCheckStates.get(position));
        } else if (getContext().getApplicationInfo().targetSdkVersion
                >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            child.setActivated(mCheckStates.get(position));
        }
    }

    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);

        // If the view was previously attached for a different position,
        // then manually jump the drawables.
        if (isAttachedToWindow
                && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                        != position) {
            child.jumpDrawablesToCurrentState();
        }
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        // add view in layout will reset the RTL properties. We have to re-resolve them
        child.resolveRtlPropertiesIfNeeded();
    }

    if (needToMeasure) {
        final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        final int lpHeight = p.height;
        final int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
                    MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        cleanupLayoutState(child);
    }

    final int w = child.getMeasuredWidth();
    final int h = child.getMeasuredHeight();
    final int childTop = flowDown ? y : y - h;

    if (needToMeasure) {
        final int childRight = childrenLeft + w;
        final int childBottom = childTop + h;
        child.layout(childrenLeft, childTop, childRight, childBottom);
    } else {
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }

    if (mCachingStarted && !child.isDrawingCacheEnabled()) {
        child.setDrawingCacheEnabled(true);
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

setupChild方法的传入参数中,isAttachedToWindow传入的是mIsScrap[0],该值在obtainView方法中设置为了false。所以,我们略过一些状态的判定,可以直接来到59行的if语句,显然条件都不满足,因此会走70行的else分支。在该分支中会调用ViewGroup.addViewInLayout方法将child添加到ListView中。
同时,由于isAttachedToWindow为false,所以needToMeasure初始化为了true。因此,child会在第91行完成measure操作,在103行完成layout操作。

值得一提的是,在层层返回到layoutChildren方法之后,方法会继续执行到第105行的recycleBin.scrapActiveViews();语句,该方法会将ActionViews所有剩下的View移动到ScrapView集合中。也就是说,ActionViews的生命周期仅仅只存在于layout过程中。

至此,ListView初次layout的缓存流程已经探究完毕。下面我们看再次layout时的缓存流程。

1.3 ListView再次layout

再次layout主要是探究有缓存的情况下,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 = 0;
        int delta = 0;

        View sel;
        View oldSel = null;
        View oldFirst = null;
        View newSel = null;

        ...

        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);

        ...

        // 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_MOVE_SELECTION:
            ...
        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();

        // remove any header/footer that has been temp detached and not re-attached
        removeUnusedFixedViews(mHeaderViewInfos);
        removeUnusedFixedViews(mFooterViewInfos);

        ...

        // Tell focus view we are done mucking with it, if it is still in
        // our view hierarchy.
        if (focusLayoutRestoreView != null
                && focusLayoutRestoreView.getWindowToken() != null) {
            focusLayoutRestoreView.dispatchFinishTemporaryDetach();
        }

        mLayoutMode = LAYOUT_NORMAL;
        mDataChanged = false;
        if (mPositionScrollAfterLayout != null) {
            post(mPositionScrollAfterLayout);
            mPositionScrollAfterLayout = null;
        }
        mNeedSync = false;
        setNextSelectedPositionInt(mSelectedPosition);

        updateScrollIndicators();

        if (mItemCount > 0) {
            checkSelectionChanged();
        }

        invokeOnItemScrollListener();
    } finally {
        if (mFocusSelector != null) {
            mFocusSelector.onLayoutComplete();
        }
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

还是和初次layout一样,会执行第68行的recycleBin.fillActiveViews方法,不过由于此时ListView中有View了,所以这些子View都会被缓存到RecycleBin中。接着执行第72行的detachAllViewsFromParent方法将children的mParent以及自己置为null,这样会暂时从ListView中detach,待稍后在复用过程中调用attachViewToParent方法重新attach。
接着来到了第75行的switch,我们还是进入了default分支。由于此时childCount不为0,所以会走else分支。又由于mSelectedPosition默认为INVALID_POSITION即-1,所以会走94行的分支,执行fillSpecific操作。

/**
  * Put a specific item at a specific location on the screen and then build
  * up and down from there.
  *
  * @param position The reference view to use as the starting point
  * @param top Pixel offset from the top of this view to the top of the
  *        reference view.
  *
  * @return The selected view, or null if the selected view is outside the
  *         visible area.
  */
private View fillSpecific(int position, int top) {
    boolean tempIsSelected = position == mSelectedPosition;
    View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
    // Possibly changed again in fillUp if we add rows above this one.
    mFirstPosition = position;

    View above;
    View below;

    final int dividerHeight = mDividerHeight;
    if (!mStackFromBottom) {
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        // This will correct for the top of the first view not touching the top of the list
        adjustViewsUpOrDown();
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
            correctTooHigh(childCount);
        }
    } else {
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        // This will correct for the bottom of the last view not touching the bottom of the list
        adjustViewsUpOrDown();
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
                correctTooLow(childCount);
        }
    }

    if (tempIsSelected) {
        return temp;
    } else if (above != null) {
        return above;
    } else {
        return below;
    }
}

fillSpecific()方法和fillUp()fillDown()方法功能差不多,都是fill操作,不同的是fillSpecific()方法会先将指定位置的子View先加载到屏幕上,然后再从该View往上以及往下fill其它子View。
这里我们直接关注重点方法——makeAndAddView

/**
  * Obtains the view and adds it to our list of children. The view can be
  * made fresh, converted from an unused view, or used as is if it was in
  * the recycle bin.
  *
  * @param position logical position in the list
  * @param y top or bottom edge of the view to add
  * @param flow {@code true} to align top edge to y, {@code false} to align
  *             bottom edge to y
  * @param childrenLeft left edge where children should be positioned
  * @param selected {@code true} if the position is selected, {@code false}
  *                 otherwise
  * @return the view that was added
  */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

还是先尝试从ActiveViews中获取View,显然这次可以了。接着调用第23行的setupChild方法并返回了actionView。既然如何,ListView就不会执行下面第30行的obtainView方法了,因为ActiveViews中获取的View肯定是上一刻还显示在屏幕上的,无需让Adapter再次inflate布局或者重新更新的UI值。

我们注意到上面第23行的setupChild方法中,最后一个参数为true,该参数是isAttachedToWindow。因此needToMeasure为false,表示child不需要重新measure、layout;另外,也会导致下面第59行的if为true,进而导致调用attachViewToParent方法,让child重新attach到ListView上:

/**
  * Adds a view as a child and make sure it is measured (if necessary) and
  * positioned properly.
  *
  * @param child the view to add
  * @param position the position of this child
  * @param y the y position relative to which this view will be positioned
  * @param flowDown {@code true} to align top edge to y, {@code false} to
  *                 align bottom edge to y
  * @param childrenLeft left edge where children should be positioned
  * @param selected {@code true} if the position is selected, {@code false}
  *                 otherwise
  * @param isAttachedToWindow {@code true} if the view is already attached
  *                           to the window, e.g. whether it was reused, or
  *                           {@code false} otherwise
  */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

    final boolean isSelected = selected && shouldShowSelector();
    final boolean updateChildSelected = isSelected != child.isSelected();
    final int mode = mTouchMode;
    final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
            && mMotionPosition == position;
    final boolean updateChildPressed = isPressed != child.isPressed();
    final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
            || child.isLayoutRequested();

    // Respect layout params that are already in the view. Otherwise make
    // some up...
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    }
    p.viewType = mAdapter.getItemViewType(position);
    p.isEnabled = mAdapter.isEnabled(position);

    // Set up view state before attaching the view, since we may need to
    // rely on the jumpDrawablesToCurrentState() call that occurs as part
    // of view attachment.
    if (updateChildSelected) {
        child.setSelected(isSelected);
    }

    if (updateChildPressed) {
        child.setPressed(isPressed);
    }

    if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
        if (child instanceof Checkable) {
            ((Checkable) child).setChecked(mCheckStates.get(position));
        } else if (getContext().getApplicationInfo().targetSdkVersion
                >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            child.setActivated(mCheckStates.get(position));
        }
    }

    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);

        // If the view was previously attached for a different position,
        // then manually jump the drawables.
        if (isAttachedToWindow
                && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                        != position) {
            child.jumpDrawablesToCurrentState();
        }
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        // add view in layout will reset the RTL properties. We have to re-resolve them
        child.resolveRtlPropertiesIfNeeded();
    }

    if (needToMeasure) {
        final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        final int lpHeight = p.height;
        final int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
                    MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        cleanupLayoutState(child);
    }

    final int w = child.getMeasuredWidth();
    final int h = child.getMeasuredHeight();
    final int childTop = flowDown ? y : y - h;

    if (needToMeasure) {
        final int childRight = childrenLeft + w;
        final int childBottom = childTop + h;
        child.layout(childrenLeft, childTop, childRight, childBottom);
    } else {
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }

    if (mCachingStarted && !child.isDrawingCacheEnabled()) {
        child.setDrawingCacheEnabled(true);
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

回想一下第59行if的意义

  • 第一次layout时由于没有可用的缓存,所以创建了新的View并使isAttachedToWindow为false,这样会调用addViewInLayout方法向ListView添加一个新View
  • 第二次layout由于有可用的缓存,且缓存View由于detachAllViewsFromParent()方法的调用从而暂时处于detach状态;接着在成功复用到了View后,调用setupChild方法时传入参数isAttachedToWindow为true,这样就会执行attachViewToParent方法了,使child恢复了attach状态

至此第二次layout过程结束了,下面研究一下用户滑动ListView时发生了什么。

1.4 用户滑动

ListViewonTouchEvent实现是在AbsListView中,也就是说GridView也有着同样的机制。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (!isEnabled()) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return isClickable() || isLongClickable();
    }

    if (mPositionScroller != null) {
        mPositionScroller.stop();
    }

    if (mIsDetaching || !isAttachedToWindow()) {
        // Something isn't right.
        // Since we rely on being attached to get data set change notifications,
        // don't risk doing anything where we might try to resync and find things
        // in a bogus state.
        return false;
    }

    startNestedScroll(SCROLL_AXIS_VERTICAL);

    if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
        return true;
    }

    initVelocityTrackerIfNotExists();
    final MotionEvent vtev = MotionEvent.obtain(ev);

    final int actionMasked = ev.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }
    vtev.offsetLocation(0, mNestedYOffset);
    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            onTouchDown(ev);
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }

        case MotionEvent.ACTION_UP: {
            onTouchUp(ev);
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            onTouchCancel();
            break;
        }

        case MotionEvent.ACTION_POINTER_UP: {
            onSecondaryPointerUp(ev);
            final int x = mMotionX;
            final int y = mMotionY;
            final int motionPosition = pointToPosition(x, y);
            if (motionPosition >= 0) {
                // Remember where the motion event started
                final View child = getChildAt(motionPosition - mFirstPosition);
                mMotionViewOriginalTop = child.getTop();
                mMotionPosition = motionPosition;
            }
            mLastY = y;
            break;
        }

        case MotionEvent.ACTION_POINTER_DOWN: {
            // New pointers take over dragging duties
            final int index = ev.getActionIndex();
            final int id = ev.getPointerId(index);
            final int x = (int) ev.getX(index);
            final int y = (int) ev.getY(index);
            mMotionCorrection = 0;
            mActivePointerId = id;
            mMotionX = x;
            mMotionY = y;
            final int motionPosition = pointToPosition(x, y);
            if (motionPosition >= 0) {
                // Remember where the motion event started
                final View child = getChildAt(motionPosition - mFirstPosition);
                mMotionViewOriginalTop = child.getTop();
                mMotionPosition = motionPosition;
            }
            mLastY = y;
            break;
        }
    }

    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
}

MotionEvent的事件种类太多了,而我们只关心移动的事件,所以直接看onTouchMove方法即可:

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    if (mHasPerformedLongPress) {
        // Consume all move events following a successful long press.
        return;
    }

    int pointerIndex = ev.findPointerIndex(mActivePointerId);
    if (pointerIndex == -1) {
        pointerIndex = 0;
        mActivePointerId = ev.getPointerId(pointerIndex);
    }

    if (mDataChanged) {
        // Re-sync everything if data has been changed
        // since the scroll operation can query the adapter.
        layoutChildren();
    }

    final int y = (int) ev.getY(pointerIndex);

    switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            // Check if we have moved far enough that it looks more like a
            // scroll than a tap. If so, we'll enter scrolling mode.
            if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                break;
            }
            // Otherwise, check containment within list bounds. If we're
            // outside bounds, cancel any active presses.
            final View motionView = getChildAt(mMotionPosition - mFirstPosition);
            final float x = ev.getX(pointerIndex);
            if (!pointInView(x, y, mTouchSlop)) {
                setPressed(false);
                if (motionView != null) {
                    motionView.setPressed(false);
                }
                removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                        mPendingCheckForTap : mPendingCheckForLongPress);
                mTouchMode = TOUCH_MODE_DONE_WAITING;
                updateSelectorState();
            } else if (motionView != null) {
                // Still within bounds, update the hotspot.
                final float[] point = mTmpPoint;
                point[0] = x;
                point[1] = y;
                transformPointToViewLocal(point, motionView);
                motionView.drawableHotspotChanged(point[0], point[1]);
            }
            break;
        case TOUCH_MODE_SCROLL:
        case TOUCH_MODE_OVERSCROLL:
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;
    }
}

onTouchMove方法会对mTouchMode做一个switch,手指在屏幕上滑动时,对应的mode为TOUCH_MODE_SCROLL,所以我们直接来到了scrollIfNeeded方法。由于方法实在有点长,所以省略了不相关的一些代码:

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
    int rawDeltaY = y - mMotionY;
    int scrollOffsetCorrection = 0;
    int scrollConsumedCorrection = 0;
    if (mLastY == Integer.MIN_VALUE) {
        rawDeltaY -= mMotionCorrection;
    }
    ...
    final int deltaY = rawDeltaY;
    int incrementalDeltaY =
            mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
    int lastYCorrection = 0;

    if (mTouchMode == TOUCH_MODE_SCROLL) {
        if (PROFILE_SCROLLING) {
            if (!mScrollProfilingStarted) {
                Debug.startMethodTracing("AbsListViewScroll");
                mScrollProfilingStarted = true;
            }
        }

        if (mScrollStrictSpan == null) {
            // If it's non-null, we're already in a scroll.
            mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");
        }

        if (y != mLastY) {
            // We may be here after stopping a fling and continuing to scroll.
            // If so, we haven't disallowed intercepting touch events yet.
            // Make sure that we do so in case we're in a parent that can intercept.
            if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
                    Math.abs(rawDeltaY) > mTouchSlop) {
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }

            final int motionIndex;
            if (mMotionPosition >= 0) {
                motionIndex = mMotionPosition - mFirstPosition;
            } else {
                // If we don't have a motion position that we can reliably track,
                // pick something in the middle to make a best guess at things below.
                motionIndex = getChildCount() / 2;
            }

            int motionViewPrevTop = 0;
            View motionView = this.getChildAt(motionIndex);
            if (motionView != null) {
                motionViewPrevTop = motionView.getTop();
            }

            // No need to do all this work if we're not going to move anyway
            boolean atEdge = false;
            if (incrementalDeltaY != 0) {
                atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
            }

            // Check to see if we have bumped into the scroll limit
            motionView = this.getChildAt(motionIndex);
            if (motionView != null) {
                // Check if the top of the motion view is where it is
                // supposed to be
                final int motionViewRealTop = motionView.getTop();
                if (atEdge) {
                    // Apply overscroll

                    int overscroll = -incrementalDeltaY -
                            (motionViewRealTop - motionViewPrevTop);
                    if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll,
                            mScrollOffset)) {
                        lastYCorrection -= mScrollOffset[1];
                        if (vtev != null) {
                            vtev.offsetLocation(0, mScrollOffset[1]);
                            mNestedYOffset += mScrollOffset[1];
                        }
                    } else {
                        final boolean atOverscrollEdge = overScrollBy(0, overscroll,
                                0, mScrollY, 0, 0, 0, mOverscrollDistance, true);

                        if (atOverscrollEdge && mVelocityTracker != null) {
                            // Don't allow overfling if we're at the edge
                            mVelocityTracker.clear();
                        }

                        final int overscrollMode = getOverScrollMode();
                        if (overscrollMode == OVER_SCROLL_ALWAYS ||
                                (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
                                        !contentFits())) {
                            if (!atOverscrollEdge) {
                                mDirection = 0; // Reset when entering overscroll.
                                mTouchMode = TOUCH_MODE_OVERSCROLL;
                            }
                            if (incrementalDeltaY > 0) {
                                mEdgeGlowTop.onPull((float) -overscroll / getHeight(),
                                        (float) x / getWidth());
                                if (!mEdgeGlowBottom.isFinished()) {
                                    mEdgeGlowBottom.onRelease();
                                }
                                invalidateTopGlow();
                            } else if (incrementalDeltaY < 0) {
                                mEdgeGlowBottom.onPull((float) overscroll / getHeight(),
                                        1.f - (float) x / getWidth());
                                if (!mEdgeGlowTop.isFinished()) {
                                    mEdgeGlowTop.onRelease();
                                }
                                invalidateBottomGlow();
                            }
                        }
                    }
                }
                mMotionY = y + lastYCorrection + scrollOffsetCorrection;
            }
            mLastY = y + lastYCorrection + scrollOffsetCorrection;
        }
    } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
        ...
    }
}

上面的方法中我们需要看的就是第57行的trackMotionScroll方法,由于滑动的每个事件都会触发上面的方法,所以方法会被调用很多次。trackMotionScroll代码如下:

/**
  * Track a motion scroll
  *
  * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
  *        began. Positive numbers mean the user's finger is moving down the screen.
  * @param incrementalDeltaY Change in deltaY from the previous event.
  * @return true if we're already at the beginning/end of the list and have nothing to do.
  */
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    final int childCount = getChildCount();
    if (childCount == 0) {
        return true;
    }

    final int firstTop = getChildAt(0).getTop();
    final int lastBottom = getChildAt(childCount - 1).getBottom();

    final Rect listPadding = mListPadding;

    // "effective padding" In this case is the amount of padding that affects
    // how much space should not be filled by items. If we don't clip to padding
    // there is no effective padding.
    int effectivePaddingTop = 0;
    int effectivePaddingBottom = 0;
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        effectivePaddingTop = listPadding.top;
        effectivePaddingBottom = listPadding.bottom;
    }

     // FIXME account for grid vertical spacing too?
    final int spaceAbove = effectivePaddingTop - firstTop;
    final int end = getHeight() - effectivePaddingBottom;
    final int spaceBelow = lastBottom - end;

    final int height = getHeight() - mPaddingBottom - mPaddingTop;
    if (deltaY < 0) {
        deltaY = Math.max(-(height - 1), deltaY);
    } else {
        deltaY = Math.min(height - 1, deltaY);
    }

    if (incrementalDeltaY < 0) {
        incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
    } else {
        incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
    }

    final int firstPosition = mFirstPosition;

    // Update our guesses for where the first and last views are
    if (firstPosition == 0) {
        mFirstPositionDistanceGuess = firstTop - listPadding.top;
    } else {
        mFirstPositionDistanceGuess += incrementalDeltaY;
    }
    if (firstPosition + childCount == mItemCount) {
        mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
    } else {
        mLastPositionDistanceGuess += incrementalDeltaY;
    }

    final boolean cannotScrollDown = (firstPosition == 0 &&
            firstTop >= listPadding.top && incrementalDeltaY >= 0);
    final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
            lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

    if (cannotScrollDown || cannotScrollUp) {
        return incrementalDeltaY != 0;
    }

    final boolean down = incrementalDeltaY < 0;

    final boolean inTouchMode = isInTouchMode();
    if (inTouchMode) {
        hideSelector();
    }

    final int headerViewsCount = getHeaderViewsCount();
    final int footerViewsStart = mItemCount - getFooterViewsCount();

    int start = 0;
    int count = 0;

    if (down) {
        int top = -incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top;
        }
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    } else {
        int bottom = getHeight() - incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            bottom -= listPadding.bottom;
        }
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }

    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

    mBlockLayoutRequests = true;

    if (count > 0) {
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }

    // invalidate before moving the children to avoid unnecessary invalidate
    // calls to bubble up from the children all the way to the top
    if (!awakenScrollBars()) {
        invalidate();
    }

    offsetChildrenTopAndBottom(incrementalDeltaY);

    if (down) {
        mFirstPosition += count;
    }

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }

    mRecycler.fullyDetachScrapViews();
    if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
        final int childIndex = mSelectedPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(mSelectedPosition, getChildAt(childIndex));
        }
    } else if (mSelectorPosition != INVALID_POSITION) {
        final int childIndex = mSelectorPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(INVALID_POSITION, getChildAt(childIndex));
        }
    } else {
        mSelectorRect.setEmpty();
    }

    mBlockLayoutRequests = false;

    invokeOnItemScrollListener();

    return false;
}

这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实我们就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了。如第71行所示,如果incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动。

下面将会进行一个边界值检测的过程,可以看到,从第89行开始,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,第91行当中,如果该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin.addScrapView()方法将这个View加入到scrap view当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。

接下来在第132行,会根据当前计数器的值来进行一个detach操作,它的作用就是把所有移出屏幕的子View全部detach掉。紧接着在第142行调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作为参数传入,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。

然后在第149行会进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据的。该方法是个抽象方法,需要子类实现,所以我们看看ListView.fillGap方法:

/**
  * {@inheritDoc}
  */
@Override
void fillGap(boolean down) {
    final int count = getChildCount();
    if (down) {
        int paddingTop = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingTop = getListPaddingTop();
        }
        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                paddingTop;
        fillDown(mFirstPosition + count, startOffset);
        correctTooHigh(getChildCount());
    } else {
        int paddingBottom = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingBottom = getListPaddingBottom();
        }
        final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                getHeight() - paddingBottom;
        fillUp(mFirstPosition - 1, startOffset);
        correctTooLow(getChildCount());
    }
}

无论是fillDown还是fillUp,里面都是通过makeAndAddView方法来得到View,不过此时有些不同,因为ActiveViews已经在layout过程完成后被清空了,所以会执行obtainView方法来获取view:

/**
  * Obtains the view and adds it to our list of children. The view can be
  * made fresh, converted from an unused view, or used as is if it was in
  * the recycle bin.
  *
  * @param position logical position in the list
  * @param y top or bottom edge of the view to add
  * @param flow {@code true} to align top edge to y, {@code false} to align
  *             bottom edge to y
  * @param childrenLeft left edge where children should be positioned
  * @param selected {@code true} if the position is selected, {@code false}
  *                 otherwise
  * @return the view that was added
  */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

/**
 * Gets a view and have it show the data associated with the specified
 * position. This is called when we have already discovered that the view
 * is not available for reuse in the recycle bin. The only choices left are
 * converting an old view or making a new one.
 *
 * @param position the position to display
 * @param outMetadata an array of at least 1 boolean where the first entry
 *                    will be set {@code true} if the view is currently
 *                    attached to the window, {@code false} otherwise (e.g.
 *                    newly-inflated or remained scrap for multiple layout
 *                    passes)
 *
 * @return A view displaying the data associated with the specified position
 */
View obtainView(int position, boolean[] outMetadata) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

    outMetadata[0] = false;

    // Check whether we have a transient state view. Attempt to re-bind the
    // data and discard the view if we fail.
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

        // If the view type hasn't changed, attempt to re-bind the data.
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);

            // If we failed to re-bind the data, scrap the obtained view.
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        outMetadata[0] = true;

        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }

    if (mCacheColorHint != 0) {
        child.setDrawingCacheBackgroundColor(mCacheColorHint);
    }

    ...

    setItemViewLayoutParams(child, position);

    ...

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);

    return child;
}

在上面第61行会调用RecycleBin.getTransientStateView方法获取transient状态的ScrapView,一般是没有的。所以会执行第83行的RecycleBin.getScrapView方法从ScrapViews里面获取一个View。在trackMotionScroll方法中我们会将任何移出屏幕的View添加到ScrapViews中,所以肯定是可以取到这个View的。因此,会在第84行中调用Adapter.getView方法让Adapter复用该View,并填充对应的数据,这样这个View看起来就像是全新的一样。

当然,如果复用View时,控件没有处理好,也是会出现复用引起的bug的,值得注意。一个典型的问题就是,如果某数据值为true就设置ImageView显示某个图片,但是值为false时又没有做任何处理,快速滑动时就会出现ImageView的显示与实际数据对不上的情况。

那么,以上就是ListView缓存机制的全部内容。

2. RecyclerView缓存策略

RecyclerView源码解析可以参考RecyclerView 源码分析系列

首先,我们还是看一下RecycerView典型的Adapter的实现:

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerviewAdapter.MyViewHolder> {

    private Context context;
    private List<String> data;

    public MyRecyclerViewAdapter(Context context,List<String> data){
        this.context = context;
        this.data = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.recycler_item_my, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
        holder.name.setText(data.get(position));

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("这里是点击每一行item的响应事件",""+position+item);
            }
        });

    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    public class MyViewHolder extends RecyclerView.ViewHolder{
        TextView name;

        public MyViewHolder(View itemView) {
            super(itemView);
            name = itemView.findViewById(R.id.name);
        }
    }
}

可以看到Adapter主要是两个方法在缓存中起作用:用来创建新的ViewHolder的onCreateViewHolder,以及用来显示数据的onBindViewHolder方法。

2.1 Recycler

ListView中的RecycleBin一样,RecyclerView中的缓存也由一个内部类Recycler进行管理。Recycler里面有四个不同层次的缓存,比ListView层次要丰富一点,当然,这与RecyclerView拓展性更好有一定的关系。

Recycler里面字段如下:

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

private final List<ViewHolder>
        mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;

RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;

static final int DEFAULT_CACHE_SIZE = 2;

解释如下:

  • mAttachedScrapmChangedScrap
    一级缓存,同ListViewActionViews,在layout发生前将屏幕上面的ViewHolder保存起来,供layout中进行复用
  • mCachedViews
    二级缓存,默认大小保持在DEFAULT_CACHE_SIZE = 2,可以通过RecyclerView.setItemViewCacheSize(int)方法进行设置 mCachedViews数量如果超出限制,会根据索引将里面旧的移动到RecyclerViewPool
  • ViewCacheExtension
    三级缓存,开发者可以自定义的缓存
  • RecyclerViewPool
    四级缓存,可以在多个RecyclerView中共享缓存
    根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为DEFAULT_MAX_SCRAP = 5,超过部分会丢弃,可以通过其setMaxRecycledViews(int viewType, int max)方法来控制对应type的缓存池大小。

Recycler的方法本质上就是对上面数据结构的一些操作。主要的方法有:

  • recycleView(View)
    将view对应的ViewHolder移动到mCachedViews中;如果View是scrapped状态,会先unscrap
  • recycleViewHolderInternal(ViewHolder)
    将ViewHolder保存到mCachedViews
  • addViewHolderToRecycledViewPool(ViewHolder, boolean)
    将ViewHolder保存到RecycledViewPool
  • scrapView(View)
    将一个attached状态的View保存到mAttachedScrapmChangedScrap
  • getChangedScrapViewForPosition(int)
    mChangedScrap中寻找匹配的ViewHolder
  • getScrapOrHiddenOrCachedHolderForPosition(int, boolean)
    依次从mAttachedScrapmCachedViews中寻找匹配的ViewHolder
  • getScrapOrCachedViewForId(long, int, boolean)
    依次从mAttachedScrapmCachedViews中寻找匹配的ViewHolder
  • tryGetViewHolderForPositionByDeadline(int, boolean, long)
    mChangedScrapmAttachedScrapmCachedViewsViewCacheExtensionRecycledViewPool中进行匹配;若匹配不了,最后会直接调用Adapter.createViewHolder方法进行创建
  • tryBindViewHolderByDeadline(ViewHolder, int, int, long)
    调用Adapter.bindViewHolder方法绑定View

2.2 缓存流程

RecyclerView的缓存流程同ListView一样,也是体现在了layout过程中。由于RecyclerView layout过程中步骤比较多,而这些内容不是本章的重点,所以这里只给出大致流程,重点放到缓存流程中。

RecyclerView的layout流程分为三个方法,对应layout step的三个步骤。这部分表格如下:

layout step layout过程 方法作用
State.STEP_START dispatchLayoutStep1 State.STEP_START状态可以执行,执行完毕后状态变成State.STEP_LAYOUT
1. 处理Adapter的更新,更新一些相关的值
2. 决定应该执行哪个动画
3. 保存当前View的动画信息
4. 如果有必要,执行预测性的layout并保存View动画信息
State.STEP_LAYOUT dispatchLayoutStep2 State.STEP_LAYOUT | State.STEP_ANIMATIONS状态可以执行,执行完毕后状态变成State.STEP_ANIMATIONS
真正执行views的layout;如有必要,该步骤可能执行多次
State.STEP_ANIMATIONS dispatchLayoutStep3 State.STEP_ANIMATIONS | State.STEP_ANIMATIONS状态可以执行,执行开始前状态变成State.STEP_START
执行第一步保存的View的动画信息

本章中我们关注的重点显然是在dispatchLayoutStep2方法中:

/**
  * The second layout step where we do the actual layout of the views for the final state.
  * This step might be run multiple times if necessary (e.g. measure).
  */
private void dispatchLayoutStep2() {
    eatRequestLayout();
    onEnterLayoutOrScroll();
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;

    // onLayoutChildren may have caused client code to disable item animations; re-check
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    resumeRequestLayout(false);
}

该方法比较简单,重点在第15行的mLayout.onLayoutChildren(mRecycler, mState)方法中。这里mLayout我们选择最常用的LinearLayoutManager进行分析。LinearLayoutManager.onLayoutChildren方法如下:

/**
  * {@inheritDoc}
  */
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // layout algorithm:
    // 1) by checking children and other variables, find an anchor coordinate and an anchor
    //  item position.
    // 2) fill towards start, stacking from bottom
    // 3) fill towards end, stacking from top
    // 4) scroll to fulfill requirements like stack from bottom.
    // create layout state
    if (DEBUG) {
        Log.d(TAG, "is pre layout:" + state.isPreLayout());
    }
    if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
        if (state.getItemCount() == 0) {
            removeAndRecycleAllViews(recycler);
            return;
        }
    }
    if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
        mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
    }

    ensureLayoutState();
    mLayoutState.mRecycle = false;
    // resolve layout direction
    resolveShouldLayoutReverse();

    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // calculate anchor position and coordinate
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                    >= mOrientationHelper.getEndAfterPadding()
            || mOrientationHelper.getDecoratedEnd(focused)
            <= mOrientationHelper.getStartAfterPadding())) {
        // This case relates to when the anchor child is the focused view and due to layout
        // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
        // up after tapping an EditText which shrinks RV causing the focused view (The tapped
        // EditText which is the anchor child) to get kicked out of the screen. Will update the
        // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
        // the available space in layoutState will be calculated as negative preventing the
        // focused view from being laid out in fill.
        // Note that we won't update the anchor position between layout passes (refer to
        // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
        // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
        // child which can change between layout passes).
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused);
    }
    if (DEBUG) {
        Log.d(TAG, "Anchor info:" + mAnchorInfo);
    }

    // LLM may decide to layout items for "extra" pixels to account for scrolling target,
    // caching or predictive animations.
    int extraForStart;
    int extraForEnd;
    final int extra = getExtraLayoutSpace(state);
    // If the previous scroll delta was less than zero, the extra space should be laid out
    // at the start. Otherwise, it should be at the end.
    if (mLayoutState.mLastScrollDelta >= 0) {
        extraForEnd = extra;
        extraForStart = 0;
    } else {
        extraForStart = extra;
        extraForEnd = 0;
    }
    extraForStart += mOrientationHelper.getStartAfterPadding();
    extraForEnd += mOrientationHelper.getEndPadding();
    if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION
            && mPendingScrollPositionOffset != INVALID_OFFSET) {
        // if the child is visible and we are going to move it around, we should layout
        // extra items in the opposite direction to make sure new items animate nicely
        // instead of just fading in
        final View existing = findViewByPosition(mPendingScrollPosition);
        if (existing != null) {
            final int current;
            final int upcomingOffset;
            if (mShouldReverseLayout) {
                current = mOrientationHelper.getEndAfterPadding()
                        - mOrientationHelper.getDecoratedEnd(existing);
                upcomingOffset = current - mPendingScrollPositionOffset;
            } else {
                current = mOrientationHelper.getDecoratedStart(existing)
                        - mOrientationHelper.getStartAfterPadding();
                upcomingOffset = mPendingScrollPositionOffset - current;
            }
            if (upcomingOffset > 0) {
                extraForStart += upcomingOffset;
            } else {
                extraForEnd -= upcomingOffset;
            }
        }
    }
    int startOffset;
    int endOffset;
    final int firstLayoutDirection;
    if (mAnchorInfo.mLayoutFromEnd) {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                : LayoutState.ITEM_DIRECTION_HEAD;
    } else {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
    }

    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mInfinite = resolveIsInfinite();
    mLayoutState.mIsPreLayout = state.isPreLayout();
    if (mAnchorInfo.mLayoutFromEnd) {
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        final int firstElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForEnd += mLayoutState.mAvailable;
        }
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;

        if (mLayoutState.mAvailable > 0) {
            // end could not consume all. add more items towards start
            extraForStart = mLayoutState.mAvailable;
            updateLayoutStateToFillStart(firstElement, startOffset);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    } else {
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForStart += mLayoutState.mAvailable;
        }
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;

        if (mLayoutState.mAvailable > 0) {
            extraForEnd = mLayoutState.mAvailable;
            // start could not consume all it should. add more items towards end
            updateLayoutStateToFillEnd(lastElement, endOffset);
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
        }
    }

    // changes may cause gaps on the UI, try to fix them.
    // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
    // changed
    if (getChildCount() > 0) {
        // because layout from end may be changed by scroll to position
        // we re-calculate it.
        // find which side we should check for gaps.
        if (mShouldReverseLayout ^ mStackFromEnd) {
            int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
            startOffset += fixOffset;
            endOffset += fixOffset;
            fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
            startOffset += fixOffset;
            endOffset += fixOffset;
        } else {
            int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
            startOffset += fixOffset;
            endOffset += fixOffset;
            fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
            startOffset += fixOffset;
            endOffset += fixOffset;
        }
    }
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    if (!state.isPreLayout()) {
        mOrientationHelper.onLayoutComplete();
    } else {
        mAnchorInfo.reset();
    }
    mLastStackFromEnd = mStackFromEnd;
    if (DEBUG) {
        validateChildOrder();
    }
}

方法很长,但还好有一些注释。

  1. 首先,从开头到第112行都是第一步的内容:计算锚点坐标以及锚点item的position。谁让112行是onAnchorReady方法呢,太明显了。
  2. 注意第113行的detachAndScrapAttachedViews方法,该方法会对所有的子View调用scrapOrRecycleView方法。这样所有的子View都会暂时detach掉,并保存到mAttachedScrapmChangedScrapmCachedViews中,等待后续复用。

         /**
         * Temporarily detach and scrap all currently attached child views. Views will be scrapped
         * into the given Recycler. The Recycler may prefer to reuse scrap views before
         * other views that were previously recycled.
         *
         * @param recycler Recycler to scrap views into
         */
     public void detachAndScrapAttachedViews(Recycler recycler) {
         final int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
             final View v = getChildAt(i);
             scrapOrRecycleView(recycler, i, v);
         }
     }
    
     private void scrapOrRecycleView(Recycler recycler, int index, View view) {
         final ViewHolder viewHolder = getChildViewHolderInt(view);
         if (viewHolder.shouldIgnore()) {
             if (DEBUG) {
                 Log.d(TAG, "ignoring view " + viewHolder);
             }
             return;
         }
         if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                 && !mRecyclerView.mAdapter.hasStableIds()) {
             removeViewAt(index);
             recycler.recycleViewHolderInternal(viewHolder);
         } else {
             detachViewAt(index);
             recycler.scrapView(view);
             mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
         }
     }
    

    前面提到过mCachedViews如果空间不足,会根据索引将里面旧的移动到RecyclerViewPool中,这样此方法的就将除了ViewCacheExtension之外的缓存全部囊括了。

  3. 根据计算的值,多次调用fill方法填充子View。
    显然,fill方法是新重点。该方法和ListView中的fillDown等类似,也是循环计算-填充-计算,我们直接看填充部分。填充部分调用了layoutChunk方法:该方法会首先调用LayoutState.next方法获取一个view;然后会addView,add过程中如果是detach过的,将会view重新attach到RecyclerView上,否则就是remove过了的,直接addView;最后调用measureChildWithMarginslayoutDecoratedWithMargins方法对子View进行测量、布局。layoutChunk方法代码如下:

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
    
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }
    

    很显然,缓存部分的关键就是LayoutState.next方法了:

    /**
      * Gets the view for the next element that we should layout.
      * Also updates current item index to the next item, based on {@link #mItemDirection}
      *
      * @return The next element that we should layout.
      */
    View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextViewFromScrapList();
        }
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }
    

    我们先略过mScrapList,暂时认为其为null,后面遇到再分析。所以这里调用了RecyclerView.getViewForPosition方法:

    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    

    离真相又近了一步,tryGetViewHolderForPositionByDeadline方法里面会对各级缓存进行匹配,这里分段进行解释。

    1. 如果有mChangedScrap,尝试进行匹配

       // 0) If there is a changed scrap, try to find from there
       if (mState.isPreLayout()) {
           holder = getChangedScrapViewForPosition(position);
           fromScrapOrHiddenOrCache = holder != null;
       }
      
      这里的isPreLayout()mState.mRunPredictiveAnimations有直接关系,可以看成前者的值取决与后者,该值在dispatchLayoutStep1过程中被更新;当Item发生了更新时,scrapView方法会将ViewHolder保存到mChangedScrap中去。

    2. 尝试从mAttachedScrapmCachedViews中寻找匹配的ViewHolder。找到之后会对ViewHolder做一些检查,如果不满足条件,且dryRun为false(实际上就是false),会将ViewHolder清除掉并保存到mCachedViews中。在向mCachedViews中添加缓存时,如果超过了允许的上限(即mViewCacheMax),将会把旧的缓存移动到RecycledViewPool中。

       // 1) Find by position from scrap/hidden list/cache
       if (holder == null) {
           holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
           if (holder != null) {
               if (!validateViewHolderForOffsetPosition(holder)) {
                   // recycle holder (and unscrap if relevant) since it can't be used
                   if (!dryRun) {
                       // we would like to recycle this but need to make sure it is not used by
                       // animation logic etc.
                       holder.addFlags(ViewHolder.FLAG_INVALID);
                       if (holder.isScrap()) {
                           removeDetachedView(holder.itemView, false);
                           holder.unScrap();
                       } else if (holder.wasReturnedFromScrap()) {
                           holder.clearReturnedFromScrapFlag();
                       }
                       recycleViewHolderInternal(holder);
                   }
                   holder = null;
               } else {
                   fromScrapOrHiddenOrCache = true;
               }
           }
       }
      

    3. 如果Adapter.hasStableIds()为true,会根据ItemId和ViewType在mAttachedScrapmCachedViews中寻找ViewHolder。Adapter中该属性默认为false。

       // 2) Find from scrap/cache via stable ids, if exists
       if (mAdapter.hasStableIds()) {
           holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                   type, dryRun);
           if (holder != null) {
               // update position
               holder.mPosition = offsetPosition;
               fromScrapOrHiddenOrCache = true;
           }
       }
      

    4. 如果存在ViewCacheExtension,调用ViewCacheExtension.getViewForPositionAndType寻找ViewHolder

       if (holder == null && mViewCacheExtension != null) {
           // We are NOT sending the offsetPosition because LayoutManager does not
           // know it.
           final View view = mViewCacheExtension
                   .getViewForPositionAndType(this, position, type);
           if (view != null) {
               holder = getChildViewHolder(view);
               if (holder == null) {
                   throw new IllegalArgumentException("getViewForPositionAndType returned"
                           + " a view which does not have a ViewHolder"
                           + exceptionLabel());
               } else if (holder.shouldIgnore()) {
                   throw new IllegalArgumentException("getViewForPositionAndType returned"
                           + " a view that is ignored. You must call stopIgnoring before"
                           + " returning this view." + exceptionLabel());
               }
           }
       }
      

    5. fallback到RecycledViewPool,看是否有可用的ViewHolder
       if (holder == null) { // fallback to pool
           if (DEBUG) {
               Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                       + position + ") fetching from shared pool");
           }
           holder = getRecycledViewPool().getRecycledView(type);
           if (holder != null) {
               holder.resetInternal();
               if (FORCE_INVALIDATE_DISPLAY_LIST) {
                   invalidateDisplayListInt(holder);
               }
           }
       }
      
    6. 以上都不满足,最后调用Adapter.createViewHolder创建ViewHolder
       if (holder == null) {
           long start = getNanoTime();
           if (deadlineNs != FOREVER_NS
                   && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
               // abort - we have a deadline we can't meet
               return null;
           }
           holder = mAdapter.createViewHolder(RecyclerView.this, type);
           if (ALLOW_THREAD_GAP_WORK) {
               // only bother finding nested RV if prefetching
               RecyclerView innerView = findNestedRecyclerView(holder.itemView);
               if (innerView != null) {
                   holder.mNestedRecyclerView = new WeakReference<>(innerView);
               }
           }
      
           long end = getNanoTime();
           mRecyclerPool.factorInCreateTime(type, end - start);
           if (DEBUG) {
               Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
           }
       }
      

    在获取到ViewHolder之后,如果需要bind,会调用tryBindViewHolderByDeadline方法,该方法中接着调用Adapter.bindViewHolder方法交给开发者完成绑定工作。

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException("Removed holder should be bound and it should"
                    + " come here only in pre-layout. Holder: " + holder
                    + exceptionLabel());
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    

    tryGetViewHolderForPositionByDeadline方法完成之后会一直返回到LinearLayoutManager.layoutChunk方法中,接着会根据ViewHolder的来源,该attach的attach,该addView的addView,最后measure并layout,一个子View的layout过程就完成了。

最后以一张流程图结束本节:

RecyclerView缓存流程

3. 两者在缓存方面的对比

上面两节分析了ListView与RecyclerView缓存机制的相关源码,这里总结一下。毕竟,前面的分析就是为了最后的结论。

本节来源于Android ListView与RecyclerView对比浅析--缓存机制

ListView与RecyclerView缓存机制原理大致相似:滑动过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异.(这只是缓存使用的其中一个场景,还有如刷新等)。原理图如下所示:

ListView缓存流程

两者缓存机制的对比有以下几点不同:

  1. 层级不同
    RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。

    ListView缓存层级

    缓存层级 是否需要创建 是否需要绑定 生命周期 备注
    mActionViews onLayout函数周期内 用于屏幕内itemView快速复用
    mScrapViews 与mAdapter一致,当mAdapter被更换时,mScrapViews即被清空

    RecyclerView缓存层级

    缓存层级 是否需要创建 是否需要绑定 生命周期 备注
    mAttachedScrap onLayout函数周期内 用于屏幕内ViewHolder快速复用
    mCacheViews 与mAdapter一致,当mAdapter被更换时,mCacheViews被降级至RecyclerViewPool;且容量超限时,老的会被降级到RecyclerViewPool 默认上限为2,即缓存屏幕外2个ViewHolder
    mViewCacheExtension 不直接使用,需要用户定制,默认不实现
    mRecyclerPool 与自身生命周期一致,不再被引用时即被释放 默认上限为5,可以用来实现所有RecyclerView同一个Pool

    ListView和RecyclerView缓存机制基本一致:

    1. mActiveViews和mAttachedScrap功能相似,用来快速重用屏幕上可见的列表项,而不需要重新创建和绑定
    2. mScrapViews和mCacheViews + mRecyclerPool功能相似,用来缓存离开屏幕的itemView,让即将进入屏幕的itemView复用
    3. RecyclerView的优势在于:
      • mCacheViews的使用,可以做到屏幕外的列表项在进入屏幕时也无须bindView快速重用
      • mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下(如ViewPager+多个列表页,或者竖向列表的item中嵌套有横向列表等)有优势。

    客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

  2. 缓存不同

    • RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:View + ViewHolder
    • ListView缓存View,实际使用的时候需要手动将自定义的ViewHolder添加到View的tag中

    缓存不同,二者在缓存的使用上也略有差异,具体来说:

    1. RecyclerView中mCacheViews获取时,是通过匹配pos获取目标位置的换缓存的,这样做的好处是,当数据源不变的情况下,无须重新bindView;而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新调用了Adapter的getView方法,这就必定会导致我们的bind代码执行。
    2. ListView中通过pos获取的是View;RecyclerView通过pos获取的是ViewHolder。

另外,RecyclerView更大的亮点在于提供了局部刷新的接口,这样可以避免调用许多无用的bindView。

ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是"一锅端",将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

评论