【源码解析】ListView的工作原理
一、引言
ListView组件,相信读者已经是非常熟悉了,所以本文就不再讲述如何使用ListView了,而是从源码的角度出发,去探寻ListView背后的绘制过程与缓存机制。
用到ListView,就不得不提Adapter。ListView采用了Adapter适配器模式,通过Adapter,ListView可以和数据进行交互,从而展现出各种丰富多样的列表内容。下图展示了ListView、Adapter、数据源之间的关系:

ListView依赖Adapter,而Adapter直接和数据源进行交互,从而达到ListView间接操作数据源的效果。
二、源码解析-绘制流程
从上图可知ListView和Adapter的关系为依赖关系,我们在平时使用ListView时,都需要通过调用setAdapter方法将Adapter和ListView绑定起来,我们就从setAdapter方法入手进行源码解析。
@Override
public void setAdapter(ListAdapter adapter) {
...
if (mAdapter != null) {
...
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
int position;
if (mStackFromBottom) {
position = lookForSelectablePosition(mItemCount - 1, false);
} else {
position = lookForSelectablePosition(0, true);
}
...
}
...
requestLayout();
}
在setAdapter方法中,mOldItemCount
变量将保存旧列表的item个数,然后调用了RecycleBin的setViewTypeCount方法设置item的类别个数。最后通过requestLayout方法进行UI绘制。
@Override
public void requestLayout() {
if (!mBlockLayoutRequests && !mInLayout) {
super.requestLayout();
}
}
我们在ListView的父类AbsListView中找到了requestLayout方法,该方法中继续调用了父类的requestLayout方法,从而跳转到了View的requestLayout方法。
public void requestLayout() {
...
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
mParent是ViewParent类型,而ViewParent是一个接口,其实际上的类型为ViewRootImpl类型,所以这里调用的是ViewRootImpl的requestLayout方法。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
通过scheduleTraversals方法,开始对View进行绘制。而绘制的过程就离不开三个步骤:
- onMeasure方法:测量View的大小
- onLayout方法:确定View的布局
- onDraw方法:绘制View
因为onMeasure方法主要是测量View的大小,对于ListView来说,经常用到的是Match_Parent;而onDraw方法是将View绘制到屏幕的过程,这和普通View的绘制过程差不多,所以我们着重解析一下onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
layoutChildren();
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
...
mInLayout = false;
}
ListView并没有onLayout方法,在其父类AbsListView找到,在onLayout方法中,调用了layoutChildren方法,该方法是一个空实现,具体的实现逻辑在子类中,于是我们又回到了ListView的layoutChildren方法。
@Override
protected void layoutChildren() {
...
try {
...
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
...
break;
case LAYOUT_SYNC:
...
break;
case LAYOUT_FORCE_BOTTOM:
...
break;
case LAYOUT_FORCE_TOP:
...
break;
case LAYOUT_SPECIFIC:
...
break;
case LAYOUT_MOVE_SELECTION:
...
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;
}
...
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
ListView的默认排列方式是从顶部开始往底部逐个item绘制的,所以最终会执行default的分支。第一次执行时ListView的item个数为0,也就是childCount == 0 成立,而mStackFromBottom变量表示ListView从底部开始往顶部绘制,因此这里是false,于是调用了fillFromTop方法,而fillFromTop方法又执行了fillDown方法。
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
...
while (nextTop < end && pos < mItemCount) {
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;
}
fillDown方法中主要的逻辑是while循环,nextTop表示下一个item的Top坐标,end表示ListView的高度,pos表示当前item的下标值,mItemCount表示数据集中item的个数。
while循环的判断条件告诉我们,ListView不管数据集中有多少数据,即使是一千条、一万条,它都只会绘制最多一个屏幕的数据。
在while循环中,调用了makeAndAddView方法去获取一个子item对应的View。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
final View child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
当数据有变化时,mDataChanged的值才会为true,因此第一次执行时,这里mDataChanged的值为false,mRecyler.getActiveView方法表示返回一个屏幕上的View,由于是第一次执行,这里的返回值肯定为null,因此最终会执行obtainView方法。
View obtainView(int position, boolean[] outMetadata) {
...
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
...
return child;
}
obtainView中调用了mAdapter的getView方法,这个方法我们已经很熟悉了。getView方法返回的是ListView中item对应的View。
在obtainView中返回了item对应的View后,在makeAndAddView方法中就调用了setupChild方法。
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
...
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();
}
...
}
在setupChild方法中,调用了addViewInLayout方法将ListView中item对应的View添加到了ListView中。前面说过,在fillDown方法的while循环中,在item的View将ListView控件填满后则会跳出循环,当我们滚动屏幕时,屏幕外的item才会被绘制并展示到屏幕中,而之前在屏幕内的item则被移除出去,这就涉及到了ListView的一个重要机制——缓存机制。
三、源码解析-缓存机制
上节分析了ListView的绘制流程,本节将分析ListView的缓存机制,正因为有了缓存机制,ListView才能流畅地将数据展示出来。
ListView本质上也是一个View,其中最重要的莫过于ListView的onTouchEvent方法,ListView的滑动过程就是从执行onTouchEvent开始的。
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
onTouchDown(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
...
}
...
return true;
}
onTouchEvent方法中,我们重点关注Motion.ACTION_MOVE分支,其中执行了onTouchMove方法。
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
...
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
...
break;
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}
在该方法中我们着重关注TOUCH_MODE_SCROLL分支,其中执行了scrollIfNeeded方法。当滑动ListView时,incrementalDeltaY不等于0。
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
所以在scrollIfNeeded方法中又执行了trackMotionScroll方法。
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
...
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
...
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) {
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) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
}
...
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
...
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
...
return false;
}
incrementalDeltaY表示手指距离上次坐标点竖直方向上的距离。当ListView向下滑动时,手指在竖直方向的坐标值不断减小,因此incrementalDeltaY为负值;当ListView向上滑动时,手指在竖直方向的坐标值不断增大,因此incrementalDeltaY为正值。
down等于true时,ListView向下滑动,循环遍历所有ListView的所有子View。当子View底部距离ListView顶部的距离(child.getBottom)小于滑动的Delta距离(top),则表示该子View移出了屏幕。 down等于false时,ListView向上滑动,循环遍历所有ListView的所有子View。当子View的顶部距离ListView顶部的距离(child.getTop)大于ListView的高度减去Delta值(bottom),则表示该子View移出了屏幕。
移出屏幕的子View都调用了RecycleBin的addScrapView方法。
void addScrapView(View scrap, int position) {
...
if (scrapHasTransientState) {
...
} else {
clearScrapForRebind(scrap);
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
}
}
addScrapView方法通过mCurrentScrap和mScrapViews将View加入到废弃View的列表中,以便后续能够快速获取并恢复。 调用addScrapView方法后,然后调用了detachViewsFromParent方法,该方法将上面刚移出屏幕的View从ListView中detach掉。
然后进行判断,如果ListView的第一个View的底部移入了屏幕或者最后一个View的顶部移入了屏幕,则调用fillGap方法。从该方法名也可以看出,该方法是将屏幕外的数据渲染成Item并移入到ListView中。fillGap方法是一个抽象方法,具体实现在ListView中。
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
int paddingTop = 0;
...
fillDown(mFirstPosition + count, startOffset);
} else {
...
fillUp(mFirstPosition - 1, startOffset);
}
}
其中调用了fillDown和fillUp方法,而这两个方法最终都会调用makeAndAddView方法。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
final View child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
由于该Item是从屏幕外移入到ListView的,所以mRecyler.getActiveView返回为null,因此接下来会执行obtainView方法。
View obtainView(int position, boolean[] outMetadata) {
...
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
...
return child;
}
obtainView方法中,通过调用mRecyler.getScrapView方法从废弃的View列表中获取一个scrapView,然后将该scrapView作为参数传递给Adapter的getView方法。Adapter的getView方法我们都很熟悉了,一般我们会在getView方法中将新数据重新赋值给scrapView,而getView的返回结果child就是移入到ListView的新Item。
四、总结
ListView的渲染过程最重要的是onLayout过程,其中第一次渲染由于缓存列表中没有缓存View,所以第一次调用的getView是新生成的,后续通过滑动ListView,RecyclerBin生成了缓存列表,之后可以从缓存列表中获取View,避免了重复创建View的过程,从而提供了性能。下面用一张图来总结一下ListView的缓存原理。

转载自:https://juejin.cn/post/6844903943923826702