android listview 分析,Android源码分析之ListView源码

前言

ListView 是用来展示大量数据的控件,且不会因为展示大量数据而出现内存溢出的现象,其原因是相关缓存机制保证了内存的合理使用。

ListView的使用也相对比较简单,大家也都会,现在官方基本都推荐使用RecyclerView去替代ListView,二者之间有相似之处,也有不同之处,本文先分析ListView的源码,重点是缓存的实现原理,后续再补充RecyclerView的原理分析,并将二者进行对比讨论。

ListView的使用可以参考ListView简单实用

ListView继承自AbsListView,AbsListView又继承自AdapterView,AdapterView继承自ViewGroup。

dd7bdd54feb6

ListView继承关系.png

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的过程:

dd7bdd54feb6

ListView_onLayout1.png

第二次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过程,因此效率方面并不会有什么明显的影响。

再进入判断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过程结束。

下图展示了第二次onLayout的过程:

dd7bdd54feb6

ListView_onLayout_2.png

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的情况,甚至内存都不会有所增加。

还有一点需要注意,我们将获取到的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工作原理:

dd7bdd54feb6

ListView Move

总结一下ListView的缓存原理如下:

dd7bdd54feb6

ListView cache

参考信息

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值