系列文章:
- Android源码分析之ListView源码
- Android源码分析之RecyclerView源码分析(一)——绘制流程
- Android源码分析之RecyclerView源码分析(二)——缓存机制
前言
ListView
是用来展示大量数据的控件,且不会因为展示大量数据而出现内存溢出的现象,其原因是相关缓存机制保证了内存的合理使用。
ListView的使用也相对比较简单,大家也都会,现在官方基本都推荐使用RecyclerView去替代ListView,二者之间有相似之处,也有不同之处,本文先分析ListView的源码,重点是缓存的实现原理,后续再补充RecyclerView的原理分析,并将二者进行对比讨论。
ListView的使用可以参考ListView简单实用
ListView继承自AbsListView,AbsListView又继承自AdapterView,AdapterView继承自ViewGroup。
RecycleBin机制
在ListView的缓存机制中,有一个类我们必须提前了解:RecycleBin
,它是ListView缓存的核心机制。RecycleBin是AbsListView
的一个内部类,而ListView继承AbsListView,所以ListView可以使用这个机制。下面是RecycleBin的部分关键源码:
class RecycleBin {
private RecyclerListener mRecyclerListener;
private int mFirstActivePosition;
// 存储View
private View[] mActiveViews = new View[0];
// 存储废弃View
private ArrayList[] mScrapViews;
private int mViewTypeCount;
// 存储废弃View
private ArrayList mCurrentScrap;
// Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项
// 而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//noinspection unchecked
ArrayList[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
// 第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值
// 根据传入的参数来将ListView中的指定元素存储到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();
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
// 从mActiveViews数组当中获取数据
// 该方法接收一个position参数,表示元素在ListView当中的位置
// 方法内部会自动将position值转换成mActiveViews数组对应的下标值
// mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除
// 下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用
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;
}
// 从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的
// 因此getScrapView()方法中的算法也非常简单
// 就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
// 将一个废弃的View进行缓存
// 该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕)
// 应该调用这个方法来对View进行缓存
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.scrappedFromPosition = position;
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
scrap.dispatchStartTemporaryDetach();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
}
下面就上述几个关键变量和方法做一些说明:
- mActiveViews:用来存放正在展示在屏幕上的view,从显示在屏幕山上的第一个view到最后一个view
- mScrapViews:存放可以由适配器用作convert view的view,是一个数组,数组的每个元素类型为ArrayList
- mCurrentScrap:是mScrapViews的第0个元素,当view种类数量为1时存放废弃view
- fillActiveViews():这个方法接收两个参数,第一个参数表示mActiveViews数组最小要保存的View数量,第二个参数表示ListView中第一个可见元素的position值。根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
- getActiveView():从mActiveViews数组当中取出特定元素,position参数表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。如果在mActiveViews数组中没有找到,则返回null。
- addScrapView():将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),应该调用这个方法来对View进行缓存,当view类型为1时则用mCurrentScrap存储废弃view,否则使用mScrapViews添加废弃view。
- getScrapView(): 从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回
- setViewTypeCount():Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制
RecycleBin类的核心方法和变量的解释暂且放在此处,后续讲解时会用到此处信息。
ListView的绘制流程
ListView本质上还是一个View,因此绘制的过程还是分为三步:onMeasure、onLayout、onDraw,onMeasure测出其占用屏幕空间,最大为整个屏幕,而onDraw用于将ListView内容绘制到屏幕上,在ListView中无实际意义,因为ListView本身只是提供了一种布局方式,真正的绘制是ListView中的子View完成的,因此onLayout方法是最为关键的。
onLayout方法
ListView的OnLayout实现在AbsListView中,具体源码如下:
@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();
mInLayout = false;
...
}
从上面代码可以看出,首先调用了父类的onLayout方法,再判断ListView是否发生了变化(大小,位置),如果ListView发生了变化,则changed变量为true,changed为true则强制每个子布局都进行重新绘制,还需要注意一点的是同时还进行了mRecycler.markChildrenDirty()
这个操作,其中mRecycler就是一个RecycleBin的对象,而markChildrenDirty()方法会为每一个scrap view调用forceLayout();判断完changed变量后又调用了layoutChildren()
方法,点开此方法可以发现他是一个空方法,因为每个子元素的布局实现应该由自己来实现,所以它的具体实现在ListView中。
layoutChildren方法
@Override
protected void layoutChildren() {
...
final int childCount = getChildCount();
...
boolean dataChanged = mDataChanged;
...
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
...
switch (mLayoutMode) {
...
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;
}
...
}
它的方法过长,只贴出来一部分,此方法中首先会获取子元素的数量,但此时ListView中还没有任何子View,因为数据都是由Adapter管理的,还没有展示到界面上。接着又会判断dataChanged
这个值,如果数据源发生变化则该值变为true,紧接着调用了RecycleBin的fillActiveViews()
方法;可是这时ListView中还没有子View,因此fillActiveViews的缓存功能无法起作用。
那么我们再接着往下分析,接下来又会判断mLayoutMode
的值,默认情况下该值都是LAYOUT_NORMAL
,此模式下会直接进入default
语句中,其中有多次if条件判断,因为当前ListView中还没有任何子View所以当前childCount
数量为0,mStackFromBottom
变量代表的是布局的顺序,默认的布局顺序是从上至下,因此会进入fillFromTop
方法中
fillFromTop方法
此方法具体代码如下:
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);
}
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;
}
fillFromTop首先计算出了mFirstPosition的值,并从mFirstPosition开始自顶至下调用fillDown
填充。
fillDown
中采用了while循环来填充,一开始时nextTop
的值是第一个子元素顶部距离整个ListView顶部的像素值,pos
是传入的mFirstPosition
的值,end是ListView底部减去顶部所得的像素值,mItemCount
是Adapter中的元素数量,因此nextTop是小于end的,pos也小于mItemCount,每次执行while循环时,pos加1,nextTop也会累加当nextTop大于end时,也就是子元素超出屏幕了,或者pos大于mItemCount时,即Adapter中所有元素都被遍历了,出现以上两种情况中一种便会跳出while循环。
在此while循环中,我们注意到调用了makeAddView
这个方法,下面具体分析下。
makeAddView方法
首先贴下代码:
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;
}
当Adapter的数据源未发生变化时,会从RecycleBin中获取一个activeView,但是目前RecycleBin中还没有缓存任何的View,因此这里得到的child为null,接着又调用了obtainView
方法来获取一个View,再来看看这个方法吧,源码如下:
obtainView方法
View obtainView(int position, boolean[] outMetadata) {
outMetadata[0] = false;
...
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();
}
}
...
return child;
}
首先调用了RecycleBin
的getScrapView
方法来尝试获取一个废弃缓存中的View,但是这里是获取不到的;接着又调用了getView
方法,即自定的Adapter中的getView方法,getView方法接收三个参数,第一个是当前子元素位置,第二个参数是convertView
,上面传入的是null,说明没有covertView可以利用,因此在Adapter中判断convertView为null时可以调用LayoutInflater的inflate
方法去加载一个布局,并将此view返回。
同时我们可以看到,这个view最终也会作为obtainView方法的返回结果,并传入makeAddView方法中后续调用的setupChild()
方法中,上面过程可以说明第一次layout过程中,所有子View都是调用LayoutInflater的inflate方法动态加载对应布局而产生的,解析布局的过程肯定是耗时的,但是在后续过程中,这种情况不会出现了。接下来,继续看下setupChild方法源码:
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
...
addViewInLayout(child, flowDown ? -1 : 0, p, true);
....
}
在setupChild
方法中会调用addViewInLayout
方法将它添加到ListView中,那么回到fillDown
方法,其中的while循环就会让子元素View将整个ListView控件填满然后跳出,也就是说即使Adapter中有很多条数据,ListView也只会加载第一屏数据。下图是第一次onLayout的过程:
第二次onLayout
即使是一个再简单的View,在展示到界面上之前都会经历至少两次onMeasure()
和两次onLayout()
的过程,自然ListView的绘制过程也不例外,同样我们关注的重点还是onLayout过程。
首先还是从layoutChildren()
方法看起:
layoutChildren方法
再来看一遍该方法源码:
@Override
protected void layoutChildren() {
...
final int childCount = getChildCount();
...
boolean dataChanged = mDataChanged;
...
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();
switch (mLayoutMode) {
...
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;
}
...
}
首先还是会获取子元素的数量,不同于第一次onLayout,此时获取到的子View数量不再为0,而是ListView中显示的子元素数量;下面又调用了RecycleBin
的fillActiveViews()
方法,目前ListView已经有子View了,这样所有的子View都会被缓存到RecycleBin中的mActiveViews
数组中,后面会使用到他们。
接下来有一个重要的方法:detachAllViewsFromParent()
,这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据,因为layoutChildren方法会向ListView中添加View,在第一次layout中已经添加了一次,如果第二次layout继续添加,那么必然会出现数据重复的问题,因此这里先调用detachAllViewsFromParent方法将第一次添加的View清除掉。
这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,我们刚刚调用了RecycleBin
的fillActiveViews()
方法来缓存子View,等会将直接使用这些缓存好的View来进行添加子View,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。
摘自Android ListView工作原理完全解析,带你从源码的角度彻底理解
再进入判断childCount
是否为0的逻辑中,此时会走和第一次layout相反的else逻辑分支,这其中又有三条逻辑分支,第一条一般不成立,因为开始时我们还没选中任何子View,第二条一般成立,mFirstPosition开始时为0,只要Adapter中数据量大于0即可,所以进入了fillSpecific
方法:
fillSpecific方法
此方法源码如下:
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就是第一个子元素的位置,因此此时其效果和上述的fillDown()基本一致。
此外可以看到,fillSpecific()
方法中也调用了makeAndAddView()
方法,因为我们之前调用detachAllViewsFromParent()
方法把所有ListView当中的子View全部清除掉了,这里肯定要重新再加上,在makeAndAddView()
方法中:
makeAndAddView方法
再来看一遍此方法源码:
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;
}
首先还是会从RecycleBin中获取ActiveView,不同于第一次layout,这次能获取到了,那肯定就不会进入obtainView中了,而是直接调用setupChild()
方法,此时setupChild()
方法的最后一个参数是true,表明当前的view是被回收过的,再来看看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);
...
} else {
...
}
...
}
可以看到,setupChild()
方法的最后一个参数是isAttachedToWindow
,方法执行过程中会对这个变量进行判断,由于isAttachedToWindow
现在是true,所以会执行attachViewToParent()
方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()
方法。
这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()
方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()
方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()
方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()
方法是正确的选择。
经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。
摘自Android ListView工作原理完全解析,带你从源码的角度彻底理解
下图展示了第二次onLayout的过程:
ListView如何做到滑动加载更多子View?
onTouchEvent方法
通过以上两次layout,我们已经能看到ListView的第一屏内容了,但是如果Adapter中有大量数据,剩下的数据怎么加载呢?我们知道实际使用过程中,ListView滑动的时候剩余的数据便显示出来了,那滑动首先肯定要监听触摸事件,相关代码在AbsListView
中的onTouchEvent
中:
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
...
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
...
}
...
return true;
}
我们主要关注ACTION_MOVE
滑动事件,因为ListView是随着滑动而加载更多子View的,其中调用了onTouchMove
方法:
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
...
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
...
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}
此方法中判断了mTouchMode
,当手指在屏幕上滑动时,TouchMode
是等于TOUCH_MODE_SCROLL
,接下来调用了scrollIfNeeded()
方法:
scrollIfNeeded方法
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
...
final int deltaY = rawDeltaY;
int incrementalDeltaY =
mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
int lastYCorrection = 0;
if (mTouchMode == TOUCH_MODE_SCROLL) {
...
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
...
}
}
...
}
注意到其中会调用trackMotionScroll
方法,只要我们手指滑动了一点距离,此方法就会被调用,自然如果手指在屏幕上滑动了很多,此方法就会被调用很多次。
trackMotionScroll方法
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
...
final boolean down = incrementalDeltaY < 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);
}
}
}
}
...
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
...
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
...
return false;
}
这个方法接收两个参数,一个是deltaY,表示手指从按下的位置到当前手指位置的Y方向的距离,incrementalDeltaY
则表示距离上次触发event事件手指在Y方向上位置的改变量,可以通过incrementalDeltaY
的正负知道用户是往上还是往下滑动。
如果incrementalDeltaY<0
,说明是向下滑动,进入if (down)
分支中,其中有个for循环,从上往下获取子View,如果子View的bottom小于ListView的Top说明这个子View已经移出屏幕了,则调用RecycleBin的addScrapView
方法将其加入到废弃缓存中,并将计数器count+1,计数器用于记录有多少个子View被移出了屏幕。
那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。
接下来,如果count大于0,说明有子View被加入废弃缓存了,则会调用detachViewsFromParent()
方法将所有移出屏幕的子View全部detach掉。有View被移出,那么自然就需要添加新的View,所以如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()
方法,fillGap()
方法是用来加载屏幕外数据的,如下所示:
fillGap方法
此方法的实现在ListView中,
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());
}
}
fillGap
接受一个down参数,此参数代表之前ListView是向下还是向上滑动,如果向下则调用fillDown()
方法,如果向上滑动则调用fillUp()
方法,这两个方法之前已经说过了,内部有一个while循环来对ListView进行填充,填充的过程是通过makeAndAddView
来实现的,好吧,再去makeAndAddView
方法中看看。
makeAndAddView方法
这是第三次来看此方法源码了:
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;
}
首先还是从RecycleBin
中获取activeView
,不过此时已经获取不到了,因为第二次layout过程中被获取过了,所以只好调用obtainView
方法了。
obtainView方法
View obtainView(int position, boolean[] outMetadata) {
...
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
...
return child;
}
这里会调用mRecycler.getScrapView
方法来获取废弃的缓存View,而刚好我们前面在trackMotionScroll
方法中处理已移出屏幕的View时将其加入废弃缓存view中了,也就是说一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()
方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。
所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。
摘自Android ListView工作原理完全解析,带你从源码的角度彻底理解
还有一点需要注意,我们将获取到的scrapView
传入了mAdapter.getView()
方法中,那么这个参数具体是什么用呢,我们来看一个Adapter的getView例子:
public View getView(int position, View convertView, ViewGroup parent) {
String url = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
} else {
view = convertView;
}
...
return view;
}
第二个参数就是convertView
,所以当这个参数不为null的时候,我们会直接复用此View,然后更新下对应的数据即可,而为null时才去加载布局文件。
再之后的代码就是调用setupChild()
方法,将获取到的view重新attach到ListView当中,因为废弃缓存中的View也是之前从ListView中detach掉的。
至此已经基本将ListView的工作原理说清楚了,下图是滑动时ListView工作原理:
总结一下ListView的缓存原理如下:
参考信息
- Android ListView工作原理完全解析,带你从源码的角度彻底理解