Android ListView工作原理完全解析,带你从源码的角度彻底理解(一)

1,049 阅读19分钟

在 Android 所有常用的原生控件当中,用法最复杂的应该就是 ListView 了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。ListView 可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。

另外 ListView 还有一个非常神奇的功能,我相信大家应该都体验过,即使在 ListView 中加载非常非常多的数据,比如达到成百上千条甚至更多,ListView 都不会发生 OOM 或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么 ListView 是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把 ListView 的源码通读了一遍,基本了解了它的工作原理,在感叹 Google 大神能够写出如此精妙代码的同时我也有所敬畏,因为 ListView 的代码量比较大,复杂度也很高,很难用文字表达清楚,于是我就放弃了把它写成一篇博客的想法。那么现在回想起来这件事我已经肠子都悔青了,因为没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。于是现在我又重新定下心来再次把 ListView 的源码重读了一遍,那么这次我一定要把它写成一篇博客,分享给大家的同时也当成我自己的笔记吧。

首先我们先来看一下 ListView 的继承结构,如下图所示:

可以看到,ListView 的继承结构还是相当复杂的,它是直接继承自的 AbsListView,而 AbsListView 有两个子实现类,一个是 ListView,另一个就是 GridView,因此我们从这一点就可以猜出来,ListView 和 GridView 在工作原理和实现上都是有很多共同点的。然后 AbsListView 又继承自 AdapterView,AdapterView 继承自 ViewGroup,后面就是我们所熟知的了。先把 ListView 的继承结构了解一下,待会儿有助于我们更加清晰地分析代码。

Adapter 的作用

Adapter 相信大家都不会陌生,我们平时使用 ListView 的时候一定都会用到它。那么话说回来大家有没有仔细想过,为什么需要 Adapter 这个东西呢?总感觉正因为有了 Adapter,ListView 的使用变得要比其它控件复杂得多。那么这里我们就先来学习一下 Adapter 到底起到了什么样的一个作用。

其实说到底,控件就是为了交互和展示数据用的,只不过 ListView 更加特殊,它是为了展示很多很多数据用的,但是 ListView 只承担交互和展示工作而已,至于这些数据来自哪里,ListView 是不关心的。因此,我们能设想到的最基本的 ListView 工作模式就是要有一个 ListView 控件和一个数据源。

不过如果真的让 ListView 和数据源直接打交道的话,那 ListView 所要做的适配工作就非常繁杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据而已,至于这个数据源到底是什么样类型,并没有严格的定义,有可能是数组,也有可能是集合,甚至有可能是数据库表中查询出来的游标。所以说如果 ListView 真的去为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它本身应该负责的工作范围,不再是仅仅承担交互和展示工作就可以了,这样 ListView 就会变得比较臃肿。

那么显然 Android 开发团队是不会允许这种事情发生的,于是就有了 Adapter 这样一个机制的出现。顾名思义,Adapter 是适配器的意思,它在 ListView 和数据源之间起到了一个桥梁的作用,ListView 并不会直接和数据源打交道,而是会借助 Adapter 这个桥梁来去访问真正的数据源,与之前不同的是,Adapter 的接口都是统一的,因此 ListView 不用再去担心任何适配方面的问题。而 Adapter 又是一个接口 (interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说 ArrayAdapter 可以用于数组和 List 类型的数据源适配,SimpleCursorAdapter 可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示:

当然 Adapter 的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我们在 Adapter 当中去重写,就是 getView() 方法,这个在下面的文章中还会详细讲到。

RecycleBin 机制

那么在开始分析 ListView 的源码之前,还有一个东西是我们提前需要了解的,就是 RecycleBin 机制,这个机制也是 ListView 能够实现成百上千条数据都不会 OOM 最重要的一个原因。其实 RecycleBin 的代码并不多,只有 300 行左右,它是写在 AbsListView 中的一个内部类,所以所有继承自 AbsListView 的子类,也就是 ListView 和 GridView,都可以使用这个机制。那我们来看一下 RecycleBin 中的主要代码,如下所示:

/**
 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
 * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
 * those views which were onscreen at the start of a layout. By
 * construction, they are displaying current information. At the end of
 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
 * are old views that could potentially be used by the adapter to avoid
 * allocating views unnecessarily.
 * 
 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
 * @see android.widget.AbsListView.RecyclerListener
 */
class RecycleBin {
	private RecyclerListener mRecyclerListener;
 
	/**
	 * 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;
 
	/**
	 * Fill ActiveViews with all of the children of the AbsListView.
	 * 
	 * @param childCount
	 *            The minimum number of views mActiveViews should hold
	 * @param firstActivePosition
	 *            The position of the first view that will be stored in
	 *            mActiveViews
	 */
	void fillActiveViews(int childCount, int firstActivePosition) {
		if (mActiveViews.length < childCount) {
			mActiveViews = new View[childCount];
		}
		mFirstActivePosition = firstActivePosition;
		final View[] activeViews = mActiveViews;
		for (int i = 0; i < childCount; i++) {
			View child = getChildAt(i);
			AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
			// Don't put header or footer views into the scrap heap
			if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
				// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
				// active views.
				// However, we will NOT place them into scrap views.
				activeViews[i] = child;
			}
		}
	}
 
	/**
	 * Get the view corresponding to the specified position. The view will
	 * be removed from mActiveViews if it is found.
	 * 
	 * @param position
	 *            The position to look up in mActiveViews
	 * @return The view if it is found, null otherwise
	 */
	View getActiveView(int position) {
		int index = position - mFirstActivePosition;
		final View[] activeViews = mActiveViews;
		if (index >= 0 && index < activeViews.length) {
			final View match = activeViews[index];
			activeViews[index] = null;
			return match;
		}
		return null;
	}
 
	/**
	 * Put a view into the ScapViews list. These views are unordered.
	 * 
	 * @param scrap
	 *            The view to add
	 */
	void addScrapView(View scrap) {
		AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
		if (lp == null) {
			return;
		}
		// Don't put header or footer views or views that should be ignored
		// into the scrap heap
		int viewType = lp.viewType;
		if (!shouldRecycleViewType(viewType)) {
			if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
				removeDetachedView(scrap, false);
			}
			return;
		}
		if (mViewTypeCount == 1) {
			dispatchFinishTemporaryDetach(scrap);
			mCurrentScrap.add(scrap);
		} else {
			dispatchFinishTemporaryDetach(scrap);
			mScrapViews[viewType].add(scrap);
		}
 
		if (mRecyclerListener != null) {
			mRecyclerListener.onMovedToScrapHeap(scrap);
		}
	}
 
	/**
	 * @return A view from the ScrapViews collection. These are unordered.
	 */
	View getScrapView(int position) {
		ArrayList<View> scrapViews;
		if (mViewTypeCount == 1) {
			scrapViews = mCurrentScrap;
			int size = scrapViews.size();
			if (size > 0) {
				return scrapViews.remove(size - 1);
			} else {
				return null;
			}
		} else {
			int whichScrap = mAdapter.getItemViewType(position);
			if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
				scrapViews = mScrapViews[whichScrap];
				int size = scrapViews.size();
				if (size > 0) {
					return scrapViews.remove(size - 1);
				}
			}
		}
		return null;
	}
 
	public void setViewTypeCount(int viewTypeCount) {
		if (viewTypeCount < 1) {
			throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
		}
		// noinspection unchecked
		ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
		for (int i = 0; i < viewTypeCount; i++) {
			scrapViews[i] = new ArrayList<View>();
		}
		mViewTypeCount = viewTypeCount;
		mCurrentScrap = scrapViews[0];
		mScrapViews = scrapViews;
	}
 
}

这里的 RecycleBin 代码并不全,我只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析 ListView 的工作原理将会有很大的帮助。

  • fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的 view 的数量,第二个参数表示 ListView 中第一个可见元素的 position 值。RecycleBin 当中使用 mActiveViews 这个数组来存储 View,调用这个方法后就会根据传入的参数来将 ListView 中的指定元素存储到 mActiveViews 数组当中。
  • getActiveView() 这个方法和 fillActiveViews() 是对应的,用于从 mActiveViews 数组当中获取数据。该方法接收一个 position 参数,表示元素在 ListView 当中的位置,方法内部会自动将 position 值转换成 mActiveViews 数组对应的下标值。需要注意的是,mActiveViews 当中所存储的 View,一旦被获取了之后就会从 mActiveViews 当中移除,下次获取同样位置的 View 将会返回 null,也就是说 mActiveViews 不能被重复利用。
  • addScrapView() 用于将一个废弃的 View 进行缓存,该方法接收一个 View 参数,当有某个 View 确定要废弃掉的时候 (比如滚动出了屏幕),就应该调用这个方法来对 View 进行缓存,RecycleBin 当中使用 mScrapViews 和 mCurrentScrap 这两个 List 来存储废弃 View。
  • getScrapView 用于从废弃缓存中取出一个 View,这些废弃缓存中的 View 是没有顺序可言的,因此 getScrapView() 方法中的算法也非常简单,就是直接从 mCurrentScrap 当中获取尾部的一个 scrap view 进行返回。
  • setViewTypeCount() 我们都知道 Adapter 当中可以重写一个 getViewTypeCount() 来表示 ListView 中有几种类型的数据项,而 setViewTypeCount() 方法的作用就是为每种类型的数据项都单独启用一个 RecycleBin 缓存机制。实际上,getViewTypeCount() 方法通常情况下使用的并不是很多,所以我们只要知道 RecycleBin 当中有这样一个功能就行了。

了解了 RecycleBin 中的主要方法以及它们的用处之后,下面就可以开始来分析 ListView 的工作原理了,这里我将还是按照以前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,不然的话要是把 ListView 所有的代码都贴出来,那么本篇文章将会很长很长了。

第一次 Layout

不管怎么说,ListView 即使再特殊最终还是继承自 View 的,因此它的执行流程还将会按照 View 的规则来执行,对于这方面不太熟悉的朋友可以参考我之前写的 Android 视图绘制流程完全解析,带你一步步深入了解 View(二) 。

View 的执行流程无非就分为三步,onMeasure() 用于测量 View 的大小,onLayout() 用于确定 View 的布局,onDraw() 用于将 View 绘制到界面上。而在 ListView 当中,onMeasure() 并没有什么特殊的地方,因为它终归是一个 View,占用的空间最多并且通常也就是整个屏幕。onDraw() 在 ListView 当中也没有什么意义,因为 ListView 本身并不负责绘制,而是由 ListView 当中的子元素来进行绘制的。那么 ListView 大部分的神奇功能其实都是在 onLayout() 方法中进行的了,因此我们本篇文章也是主要分析的这个方法里的内容。

如果你到 ListView 源码中去找一找,你会发现 ListView 中是没有 onLayout() 这个方法的,这是因为这个方法是在 ListView 的父类 AbsListView 中实现的,代码如下所示:

/**
 * 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;
	if (changed) {
		int childCount = getChildCount();
		for (int i = 0; i < childCount; i++) {
			getChildAt(i).forceLayout();
		}
		mRecycler.markChildrenDirty();
	}
	layoutChildren();
	mInLayout = false;
}

可以看到,onLayout() 方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果 ListView 的大小或者位置发生了变化,那么 changed 变量就会变成 true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第 16 行调用了 layoutChildren() 这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入 ListView 的 layoutChildren() 方法,代码如下所示:

@Override
protected void layoutChildren() {
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (!blockLayoutRequests) {
        mBlockLayoutRequests = true;
    } else {
        return;
    }
    try {
        super.layoutChildren();
        invalidate();
        if (mAdapter == null) {
            resetList();
            invokeOnItemScrollListener();
            return;
        }
        int childrenTop = mListPadding.top;
        int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
        int childCount = getChildCount();
        int index = 0;
        int delta = 0;
        View sel;
        View oldSel = null;
        View oldFirst = null;
        View newSel = null;
        View focusLayoutRestoreView = null;
        // Remember stuff we will need down below
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            index = mNextSelectedPosition - mFirstPosition;
            if (index >= 0 && index < childCount) {
                newSel = getChildAt(index);
            }
            break;
        case LAYOUT_FORCE_TOP:
        case LAYOUT_FORCE_BOTTOM:
        case LAYOUT_SPECIFIC:
        case LAYOUT_SYNC:
            break;
        case LAYOUT_MOVE_SELECTION:
        default:
            // Remember the previously selected view
            index = mSelectedPosition - mFirstPosition;
            if (index >= 0 && index < childCount) {
                oldSel = getChildAt(index);
            }
            // Remember the previous first child
            oldFirst = getChildAt(0);
            if (mNextSelectedPosition >= 0) {
                delta = mNextSelectedPosition - mSelectedPosition;
            }
            // Caution: newSel might be null
            newSel = getChildAt(index + delta);
        }
        boolean dataChanged = mDataChanged;
        if (dataChanged) {
            handleDataChanged();
        }
        // Handle the empty set by removing all views that are visible
        // and calling it a day
        if (mItemCount == 0) {
            resetList();
            invokeOnItemScrollListener();
            return;
        } else if (mItemCount != mAdapter.getCount()) {
            throw new IllegalStateException("The content of the adapter has changed but "
                    + "ListView did not receive a notification. Make sure the content of "
                    + "your adapter is not modified from a background thread, but only "
                    + "from the UI thread. [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;
        // reset the focus restoration
        View focusLayoutRestoreDirectChild = null;
        // Don't put header or footer views into the Recycler. Those are
        // already cached in mHeaderViews;
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i));
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(getChildAt(i),
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                }
            }
        } else {
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        // take focus back to us temporarily to avoid the eventual
        // call to clear focus when removing the focused child below
        // from messing things up when ViewRoot assigns focus back
        // to someone else
        final View focusedChild = getFocusedChild();
        if (focusedChild != null) {
            // TODO: in some cases focusedChild.getParent() == null
            // we can remember the focused view to restore after relayout if the
            // data hasn't changed, or if the focused position is a header or footer
            if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
                focusLayoutRestoreDirectChild = focusedChild;
                // remember the specific view that had focus
                focusLayoutRestoreView = findFocus();
                if (focusLayoutRestoreView != null) {
                    // tell it we are going to mess with it
                    focusLayoutRestoreView.onStartTemporaryDetach();
                }
            }
            requestFocus();
        }
        // Clear out old views
        detachAllViewsFromParent();
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;
        case LAYOUT_FORCE_BOTTOM:
            sel = fillUp(mItemCount - 1, childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = 0;
            sel = fillFromTop(childrenTop);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_SPECIFIC:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }
        // Flush any cached views that did not get reused above
        recycleBin.scrapActiveViews();
        if (sel != null) {
            // the current selected item should get focus if items
            // are focusable
            if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
                final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
                        focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
                if (!focusWasTaken) {
                    // selected item didn't take focus, fine, but still want
                    // to make sure something else outside of the selected view
                    // has focus
                    final View focused = getFocusedChild();
                    if (focused != null) {
                        focused.clearFocus();
                    }
                    positionSelector(sel);
                } else {
                    sel.setSelected(false);
                    mSelectorRect.setEmpty();
                }
            } else {
                positionSelector(sel);
            }
            mSelectedTop = sel.getTop();
        } else {
            if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
                View child = getChildAt(mMotionPosition - mFirstPosition);
                if (child != null) positionSelector(child);
            } else {
                mSelectedTop = 0;
                mSelectorRect.setEmpty();
            }
            // even if there is not selected position, we may need to restore
            // focus (i.e. something focusable in touch mode)
            if (hasFocus() && focusLayoutRestoreView != null) {
                focusLayoutRestoreView.requestFocus();
            }
        }
        // tell focus view we are done mucking with it, if it is still in
        // our view hierarchy.
        if (focusLayoutRestoreView != null
                && focusLayoutRestoreView.getWindowToken() != null) {
            focusLayoutRestoreView.onFinishTemporaryDetach();
        }
        mLayoutMode = LAYOUT_NORMAL;
        mDataChanged = false;
        mNeedSync = false;
        setNextSelectedPositionInt(mSelectedPosition);
        updateScrollIndicators();
        if (mItemCount > 0) {
            checkSelectionChanged();
        }
        invokeOnItemScrollListener();
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

这段代码比较长,我们挑重点的看。首先可以确定的是,ListView 当中目前还没有任何子 View,数据都还是由 Adapter 管理的,并没有展示到界面上,因此第 19 行 getChildCount() 方法得到的值肯定是 0。接着在第 81 行会根据 dataChanged 这个布尔型的值来判断执行逻辑,dataChanged 只有在数据源发生改变的情况下才会变成 true,其它情况都是 false,因此这里会进入到第 90 行的执行逻辑,调用 RecycleBin 的 fillActiveViews() 方法。按理来说,调用 fillActiveViews() 方法是为了将 ListView 的子 View 进行缓存的,可是目前 ListView 中还没有任何的子 View,因此这一行暂时还起不了任何作用。

接下来在第 114 行会根据 mLayoutMode 的值来决定布局模式,默认情况下都是普通模式 LAYOUT_NORMAL,因此会进入到第 140 行的 default 语句当中。而下面又会紧接着进行两次 if 判断,childCount 目前是等于 0 的,并且默认的布局顺序是从上往下,因此会进入到第 145 行的 fillFromTop() 方法,我们跟进去瞧一瞧:

/**
 * 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);
}

从这个方法的注释中可以看出,它所负责的主要任务就是从 mFirstPosition 开始,自顶至底去填充 ListView。而这个方法本身并没有什么逻辑,就是判断了一下 mFirstPosition 值的合法性,然后调用 fillDown() 方法,那么我们就有理由可以猜测,填充 ListView 的操作是在 fillDown() 方法中完成的。进入 fillDown() 方法,代码如下所示:

/**
 * 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 = (getBottom() - getTop()) - 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++;
    }
    return selectedView;
}

可以看到,这里使用了一个 while 循环来执行重复逻辑,一开始 nextTop 的值是第一个子元素顶部距离整个 ListView 顶部的像素值,pos 则是刚刚传入的 mFirstPosition 的值,而 end 是 ListView 底部减去顶部所得的像素值,mItemCount 则是 Adapter 中的元素数量。因此一开始的情况下 nextTop 必定是小于 end 值的,并且 pos 也是小于 mItemCount 值的。那么每执行一次 while 循环,pos 的值都会加 1,并且 nextTop 也会增加,当 nextTop 大于等于 end 时,也就是子元素已经超出当前屏幕了,或者 pos 大于等于 mItemCount 时,也就是所有 Adapter 中的元素都被遍历结束了,就会跳出 while 循环。

那么 while 循环当中又做了什么事情呢?值得让人留意的就是第 18 行调用的 makeAndAddView() 方法,进入到这个方法当中,代码如下所示:

/**
 * Obtain the view and add 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 If flow is true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @return View that was added
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

这里在第 19 行尝试从 RecycleBin 当中快速获取一个 active view,不过很遗憾的是目前 RecycleBin 当中还没有缓存任何的 View,所以这里得到的值肯定是 null。那么取得了 null 之后就会继续向下运行,到第 28 行会调用 obtainView() 方法来再次尝试获取一个 View,这次的 obtainView() 方法是可以保证一定返回一个 View 的,于是下面立刻将获取到的 View 传入到了 setupChild() 方法当中。那么 obtainView() 内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:

/**
 * Get 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 isScrap
 *            Array of at least 1 boolean, the first entry will become true
 *            if the returned view was taken from the scrap heap, false if
 *            otherwise.
 * 
 * @return A view displaying the data associated with the specified position
 */
View obtainView(int position, boolean[] isScrap) {
	isScrap[0] = false;
	View scrapView;
	scrapView = mRecycler.getScrapView(position);
	View child;
	if (scrapView != null) {
		child = mAdapter.getView(position, scrapView, this);
		if (child != scrapView) {
			mRecycler.addScrapView(scrapView);
			if (mCacheColorHint != 0) {
				child.setDrawingCacheBackgroundColor(mCacheColorHint);
			}
		} else {
			isScrap[0] = true;
			dispatchFinishTemporaryDetach(child);
		}
	} else {
		child = mAdapter.getView(position, null, this);
		if (mCacheColorHint != 0) {
			child.setDrawingCacheBackgroundColor(mCacheColorHint);
		}
	}
	return child;
}

obtainView() 方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个 ListView 中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在第 19 行代码中调用了 RecycleBin 的 getScrapView() 方法来尝试获取一个废弃缓存中的 View,同样的道理,这里肯定是获取不到的,getScrapView() 方法会返回一个 null。这时该怎么办呢?没有关系,代码会执行到第 33 行,调用 mAdapter 的 getView() 方法来去获取一个 View。那么 mAdapter 是什么呢?当然就是当前 ListView 关联的适配器了。而 getView() 方法又是什么呢?还用说吗,这个就是我们平时使用 ListView 时最最经常重写的一个方法了,这里 getView() 方法中传入了三个参数,分别是 position,null 和 this。

那么我们平时写 ListView 的 Adapter 时,getView() 方法通常会怎么写呢?这里我举个简单的例子:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	Fruit fruit = getItem(position);
	View view;
	if (convertView == null) {
		view = LayoutInflater.from(getContext()).inflate(resourceId, null);
	} else {
		view = convertView;
	}
	ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
	TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
	fruitImage.setImageResource(fruit.getImageId());
	fruitName.setText(fruit.getName());
	return view;
}

getView() 方法接受的三个参数,第一个参数 position 代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数 convertView,刚才传入的是 null,说明没有 convertView 可以利用,因此我们会调用 LayoutInflater 的 inflate() 方法来去加载一个布局。接下来会对这个 view 进行一些属性和值的设定,最后将 view 返回。

那么这个 View 也会作为 obtainView() 的结果进行返回,并最终传入到 setupChild() 方法当中。其实也就是说,第一次 layout 过程当中,所有的子 View 都是调用 LayoutInflater 的 inflate() 方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:

/**
 * Add 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 If true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @param recycled Has this view been pulled from the recycle bin? If so it
 *        does not need to be remeasured.
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean recycled) {
    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 = !recycled || updateChildSelected || child.isLayoutRequested();
    // Respect layout params that are already in the view. Otherwise make some up...
    // noinspection unchecked
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT, 0);
    }
    p.viewType = mAdapter.getItemViewType(position);
    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
            p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
    }
    if (updateChildSelected) {
        child.setSelected(isSelected);
    }
    if (updateChildPressed) {
        child.setPressed(isPressed);
    }
    if (needToMeasure) {
        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
    }
}

setupChild() 方法当中的代码虽然比较多,但是我们只看核心代码的话就非常简单了,刚才调用 obtainView() 方法获取到的子元素 View,这里在第 40 行调用了 addViewInLayout() 方法将它添加到了 ListView 当中。那么根据 fillDown() 方法中的 while 循环,会让子元素 View 将整个 ListView 控件填满然后就跳出,也就是说即使我们的 Adapter 中有一千条数据,ListView 也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证 ListView 中的内容能够迅速展示到屏幕上。

那么到此为止,第一次 Layout 过程结束。

第二次 Layout

虽然我在源码中并没有找出具体的原因,但如果你自己做一下实验的话就会发现,即使是一个再简单的 View,在展示到界面上之前都会经历至少两次 onMeasure() 和两次 onLayout() 的过程。其实这只是一个很小的细节,平时对我们影响并不大,因为不管是 onMeasure() 或者 onLayout() 几次,反正都是执行的相同的逻辑,我们并不需要进行过多关心。但是在 ListView 中情况就不一样了,因为这就意味着 layoutChildren() 过程会执行两次,而这个过程当中涉及到向 ListView 中添加子元素,如果相同的逻辑执行两遍的话,那么 ListView 中就会存在一份重复的数据了。因此 ListView 在 layoutChildren() 过程当中做了第二次 Layout 的逻辑处理,非常巧妙地解决了这个问题,下面我们就来分析一下第二次 Layout 的过程。

其实第二次 Layout 和第一次 Layout 的基本流程是差不多的,那么我们还是从 layoutChildren() 方法开始看起:

@Override
protected void layoutChildren() {
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (!blockLayoutRequests) {
        mBlockLayoutRequests = true;
    } else {
        return;
    }
    try {
        super.layoutChildren();
        invalidate();
        if (mAdapter == null) {
            resetList();
            invokeOnItemScrollListener();
            return;
        }
        int childrenTop = mListPadding.top;
        int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
        int childCount = getChildCount();
        int index = 0;
        int delta = 0;
        View sel;
        View oldSel = null;
        View oldFirst = null;
        View newSel = null;
        View focusLayoutRestoreView = null;
        // Remember stuff we will need down below
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            index = mNextSelectedPosition - mFirstPosition;
            if (index >= 0 && index < childCount) {
                newSel = getChildAt(index);
            }
            break;
        case LAYOUT_FORCE_TOP:
        case LAYOUT_FORCE_BOTTOM:
        case LAYOUT_SPECIFIC:
        case LAYOUT_SYNC:
            break;
        case LAYOUT_MOVE_SELECTION:
        default:
            // Remember the previously selected view
            index = mSelectedPosition - mFirstPosition;
            if (index >= 0 && index < childCount) {
                oldSel = getChildAt(index);
            }
            // Remember the previous first child
            oldFirst = getChildAt(0);
            if (mNextSelectedPosition >= 0) {
                delta = mNextSelectedPosition - mSelectedPosition;
            }
            // Caution: newSel might be null
            newSel = getChildAt(index + delta);
        }
        boolean dataChanged = mDataChanged;
        if (dataChanged) {
            handleDataChanged();
        }
        // Handle the empty set by removing all views that are visible
        // and calling it a day
        if (mItemCount == 0) {
            resetList();
            invokeOnItemScrollListener();
            return;
        } else if (mItemCount != mAdapter.getCount()) {
            throw new IllegalStateException("The content of the adapter has changed but "
                    + "ListView did not receive a notification. Make sure the content of "
                    + "your adapter is not modified from a background thread, but only "
                    + "from the UI thread. [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;
        // reset the focus restoration
        View focusLayoutRestoreDirectChild = null;
        // Don't put header or footer views into the Recycler. Those are
        // already cached in mHeaderViews;
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i));
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(getChildAt(i),
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                }
            }
        } else {
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        // take focus back to us temporarily to avoid the eventual
        // call to clear focus when removing the focused child below
        // from messing things up when ViewRoot assigns focus back
        // to someone else
        final View focusedChild = getFocusedChild();
        if (focusedChild != null) {
            // TODO: in some cases focusedChild.getParent() == null
            // we can remember the focused view to restore after relayout if the
            // data hasn't changed, or if the focused position is a header or footer
            if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
                focusLayoutRestoreDirectChild = focusedChild;
                // remember the specific view that had focus
                focusLayoutRestoreView = findFocus();
                if (focusLayoutRestoreView != null) {
                    // tell it we are going to mess with it
                    focusLayoutRestoreView.onStartTemporaryDetach();
                }
            }
            requestFocus();
        }
        // Clear out old views
        detachAllViewsFromParent();
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;
        case LAYOUT_FORCE_BOTTOM:
            sel = fillUp(mItemCount - 1, childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = 0;
            sel = fillFromTop(childrenTop);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_SPECIFIC:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }
        // Flush any cached views that did not get reused above
        recycleBin.scrapActiveViews();
        if (sel != null) {
            // the current selected item should get focus if items
            // are focusable
            if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
                final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
                        focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
                if (!focusWasTaken) {
                    // selected item didn't take focus, fine, but still want
                    // to make sure something else outside of the selected view
                    // has focus
                    final View focused = getFocusedChild();
                    if (focused != null) {
                        focused.clearFocus();
                    }
                    positionSelector(sel);
                } else {
                    sel.setSelected(false);
                    mSelectorRect.setEmpty();
                }
            } else {
                positionSelector(sel);
            }
            mSelectedTop = sel.getTop();
        } else {
            if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
                View child = getChildAt(mMotionPosition - mFirstPosition);
                if (child != null) positionSelector(child);
            } else {
                mSelectedTop = 0;
                mSelectorRect.setEmpty();
            }
            // even if there is not selected position, we may need to restore
            // focus (i.e. something focusable in touch mode)
            if (hasFocus() && focusLayoutRestoreView != null) {
                focusLayoutRestoreView.requestFocus();
            }
        }
        // tell focus view we are done mucking with it, if it is still in
        // our view hierarchy.
        if (focusLayoutRestoreView != null
                && focusLayoutRestoreView.getWindowToken() != null) {
            focusLayoutRestoreView.onFinishTemporaryDetach();
        }
        mLayoutMode = LAYOUT_NORMAL;
        mDataChanged = false;
        mNeedSync = false;
        setNextSelectedPositionInt(mSelectedPosition);
        updateScrollIndicators();
        if (mItemCount > 0) {
            checkSelectionChanged();
        }
        invokeOnItemScrollListener();
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

同样还是在第 19 行,调用 getChildCount() 方法来获取子 View 的数量,只不过现在得到的值不会再是 0 了,而是 ListView 中一屏可以显示的子 View 数量,因为我们刚刚在第一次 Layout 过程当中向 ListView 添加了这么多的子 View。下面在第 90 行调用了 RecycleBin 的 fillActiveViews() 方法,这次效果可就不一样了,因为目前 ListView 中已经有子 View 了,这样所有的子 View 都会被缓存到 RecycleBin 的 mActiveViews 数组当中,后面将会用到它们。

接下来将会是非常非常重要的一个操作,在第 113 行调用了 detachAllViewsFromParent() 方法。这个方法会将所有 ListView 当中的子 View 全部清除掉,从而保证第二次 Layout 过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的 View 又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了 RecycleBin 的 fillActiveViews() 方法来缓存子 View 吗,待会儿将会直接使用这些缓存好的 View 来进行加载,而并不会重新执行一遍 inflate 过程,因此效率方面并不会有什么明显的影响。

那么我们接着看,在第 141 行的判断逻辑当中,由于不再等于 0 了,因此会进入到 else 语句当中。而 else 语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition 应该等于 - 1。第二个逻辑判断通常是成立的,因为 mFirstPosition 的值一开始是等于 0 的,只要 adapter 中的数据大于 0 条件就成立。那么进入到 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() 方法功能也是差不多的,主要的区别在于,fillSpecific() 方法会优先将指定位置的子 View 先加载到屏幕上,然后再加载该子 View 往上以及往下的其它子 View。那么由于这里我们传入的 position 就是第一个子 View 的位置,于是 fillSpecific() 方法的作用就基本上和 fillDown() 方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在 makeAndAddView() 方法上面。再次回到 makeAndAddView() 方法,代码如下所示:

/**
 * Obtain the view and add 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 If flow is true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @return View that was added
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

仍然还是在第 19 行尝试从 RecycleBin 当中获取 Active View,然而这次就一定可以获取到了,因为前面我们调用了 RecycleBin 的 fillActiveViews() 方法来缓存子 View。那么既然如此,就不会再进入到第 28 行的 obtainView() 方法,而是会直接进入 setupChild() 方法当中,这样也省去了很多时间,因为如果在 obtainView() 方法中又要去 infalte 布局的话,那么 ListView 的初始加载效率就大大降低了。

注意在第 23 行,setupChild() 方法的最后一个参数传入的是 true,这个参数表明当前的 View 是之前被回收过的,那么我们再次回到 setupChild() 方法当中:

/**
 * Add 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 If true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @param recycled Has this view been pulled from the recycle bin? If so it
 *        does not need to be remeasured.
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean recycled) {
    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 = !recycled || updateChildSelected || child.isLayoutRequested();
    // Respect layout params that are already in the view. Otherwise make some up...
    // noinspection unchecked
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT, 0);
    }
    p.viewType = mAdapter.getItemViewType(position);
    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
            p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
    }
    if (updateChildSelected) {
        child.setSelected(isSelected);
    }
    if (updateChildPressed) {
        child.setPressed(isPressed);
    }
    if (needToMeasure) {
        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
    }
}

可以看到,setupChild() 方法的最后一个参数是 recycled,然后在第 32 行会对这个变量进行判断,由于 recycled 现在是 true,所以会执行 attachViewToParent() 方法,而第一次 Layout 过程则是执行的 else 语句中的 addViewInLayout() 方法。这两个方法最大的区别在于,如果我们需要向 ViewGroup 中添加一个新的子 View,应该调用 addViewInLayout() 方法,而如果是想要将一个之前 detach 的 View 重新 attach 到 ViewGroup 上,就应该调用 attachViewToParent() 方法。那么由于前面在 layoutChildren() 方法当中调用了 detachAllViewsFromParent() 方法,这样 ListView 中所有的子 View 都是处于 detach 状态的,所以这里 attachViewToParent() 方法是正确的选择。

经历了这样一个 detach 又 attach 的过程,ListView 中所有的子 View 又都可以正常显示出来了,那么第二次 Layout 过程结束。

篇幅有限,接下一篇文继续讲。