本文主要内容
Listview是一种常用的控件,它的主要特点是能够复用,上下滑动时不至于卡顿,内存波动等。要实现这种功能,肯定存在着缓存机制,今天我们着重分析下Listview的缓存机制以及它的设计模式。
- 1、Listview简介
- 2、缓存分析
- 3、Listview原理
- 4、滑动原理
- 5、总结
1、Listview简介
Listview的继承关系如下,它的父类是AbsListView,缓存类是在AbsListView中实现的。
如果是我们来设计Listview,我们会如何设计?
Listview可以上下滑动,子view被滑动出屏幕以后要如何处理?如果直接将子view删除回收,那么每次滑动都要重新inflate子view的布局,将会导致性能问题,而且内存回收也会更频繁,导致内存波动较大,因为内存并不一定会这么及时地释放。
所以需要缓存机制,将滑动出屏幕以外的子view缓存起来,为后续重用做准备,这样性能问题不存在了,内存和性能相互平衡。
另外,Listview理论上可以显示任意形式的子view,为了方便开发者,也为了解耦数据来源,封装了adapter,这是绝佳的适配器模式示例了。
2、缓存分析
Listview缓存依赖于RecycleBin类(AbsListView的内部类)。一起来看看RecycleBin类的逻辑:
class RecycleBin {
//mActiveViews中第一个view在Listview中的位置
private int mFirstActivePosition;
//缓存当前正在显示的view
private View[] mActiveViews = new View[0];
//缓存没有被显示的view,废弃view,注意它是一个List数组
private ArrayList[] mScrapViews;
//类型值,有多少种类型,mScrapViews数组的长度就是多少
private int mViewTypeCount;
//当前的废弃view缓存列表
private ArrayList mCurrentScrap;
//设置数据类型,有几种类型就有多少种废弃view缓存列表,一种类型对应一个列表
public void setViewTypeCount(int viewTypeCount) {
ArrayList[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
//将正在显示的子view都添加到mActiveViews数组中来
void fillActiveViews(int childCount, int firstActivePosition) {
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;
}
}
}
//获取某个位置上的正在显示的view
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是暂存状态,即hasTransientState为true,则不缓存
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
lp.scrappedFromPosition = position;
final int viewType = lp.viewType;
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
}
//将所有正在显示的view都添加到废弃缓存列表中
void scrapActiveViews() {
final View[] activeViews = mActiveViews;
final boolean hasListener = mRecyclerListener != null;
final boolean multipleScraps = mViewTypeCount > 1;
ArrayList scrapViews = mCurrentScrap;
final int count = activeViews.length;
for (int i = count - 1; i >= 0; i--) {
final View victim = activeViews[i];
if (victim != null) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams();
final int whichScrap = lp.viewType;
activeViews[i] = null;
lp.scrappedFromPosition = mFirstActivePosition + i;
scrapViews.add(victim);
}
}
//删除过多的废弃缓存
pruneScrapViews();
}
//删除过多的废弃缓存,因为缓存也不是无上限的,确定废弃缓存不超过mActiveViews数组的长度
private void pruneScrapViews() {
final int maxViews = mActiveViews.length;
final int viewTypeCount = mViewTypeCount;
final ArrayList[] scrapViews = mScrapViews;
for (int i = 0; i < viewTypeCount; ++i) {
final ArrayList scrapPile = scrapViews[i];
int size = scrapPile.size();
final int extras = size - maxViews;
size--;
for (int j = 0; j < extras; j++) {
removeDetachedView(scrapPile.remove(size--), false);
}
}
}
//从废弃缓存列表中取一个子view出来,注意有不同的取法,某至有匹配不到直接拿废弃列表中的最后一个
//所以adapter的getView方法中,需要对converView做检查,匹配,否则有可能出现数据错乱
private View retrieveFromScrap(ArrayList scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position or ID.
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams();
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}
}
需要注意几点:
如果子view处理TransientState状态,即hasTransientState方法返回为true,子view不会被添加到废弃列表中
二级缓存,第一重缓存为mActiveViews,代表着当前正在显示的view,第二重缓存为废弃view队列数组。它们之间的数据流转,需要查看Listview源码才能解释清楚
注意到废弃view的缓存是一个列表数组,而mActiveViews则只是一个数组。理论上废弃view缓存是一个数组就行了,为什么现在有多个废弃view队列了呢?这是因为Listview可能有多种数据类型,一种数据类型对应着一个废弃view队列。在Listview或者RecycleView中显示多种数据类型,相信很多人都用过吧。
只要是缓存,肯定有对应的缓存算法,当要缓存数量的数量超过了最大值了怎么办?要删除哪些缓存才能装得下新缓存。熟悉的有LRU算法(最近最少使用算法),而Listview比较简单,查看 pruneScrapViews 方法,只要废弃队列的长度超过了 mActiveViews 的长度,就从废弃队列最末尾开始删除,直到不超过为止。每次添加新项到废弃队列中,都会检查。
3、Listview原理
任何一个控件的原理都脱不开基本的View原理,即measure、layout、draw这一套,所以就根据这个原理,盘它!!
measure用于确定自身以及子view的大小,Listview的measure过程比较简单,自身大小受限于Listview父控件,所以measure过程不改变它。而子view的大小也根据用户的设定即可。
layout是整个过程中最复杂的一项,代码非常的长。onLayout方法中定义在父类AbsListView中,AbsListView还定义了一个抽象方法,layoutChildren,它的子类可以根据自身需求更改布局。别忘了,AbsListView还有个子类,GridView。这种设计思想值得学习,模板方法,钩子函数。
protected void layoutChildren() {
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
//如果数据改变,那么将所有子view都添加到废弃列表中
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
//如果数据不变,就将当前正在显示的所有view添加到mActiveViews中
recycleBin.fillActiveViews(childCount, firstPosition);
}
// 删除所有的子view,要重新添加
detachAllViewsFromParent();
switch (mLayoutMode) {
default:
//根据Listview的模式,确定填充子view的方法,一般情况下,是Normal模式,会走下边这个方法 fillFromTop
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
}
}
// Flush any cached views that did not get reused above
//在fillFromTop方法中,获取子view的时候其实就会从mActiveViews中拿缓存数据,如果fillFromTop方法执行完了,
// mActiveViews 中还有数据,则将剩余的数据添加到废弃队列中
//为什么 mActiveViews 中的数据没有被 fillFromTop 全被拿完呢,理论上 Listview不动,mActiveViews肯定会被拿完的
//在用户滑动列表后,mActiveViews 有可能没有被拿完
recycleBin.scrapActiveViews();
}
接着我们来看 fillFromTop 方法
private View fillDown(int pos, int nextTop) {
View selectedView = null;
//end代表着整个listview的高度
int end = (mBottom - mTop);
//nexttop表示listview的top位置
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
//nextTop的值随着子view位置的往下,而越来越大,当nextTop值大于或等于end值时,跳出循环,不再添加子view
//所有listview上只在可视区域添加子view。非可视区域并没有子view存在
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
return selectedView;
}
再看看makeAndAddView方法:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
//如果没有数据改变,直接先从 Active 缓存中拿数据,拿到了就调用setupChild方法,将子view添加到listview中来
child = mRecycler.getActiveView(position);
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
//Active缓存中没有,从废弃缓存中拿数据,然后调用adapter的getView方法,生成一个子view,将子view添加到listview上
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
setupChild 方法就不再讲述了,它的重点在于调用子view的layout方法,并且将子view添加到listview中来。
而obtainView则是获取新的子view的,它会调用adapter的getView方法,获取子view:
//注意trace操作,在抓systrace时,如果有listview,就可以看到obtainView方法执行的细节
View obtainView(int position, boolean[] isScrap) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
isScrap[0] = false;
//从废弃队列中拿缓存数据
final View scrapView = mRecycler.getScrapView(position);
//调用adapter的getView方法,给了开发一个重用的机会,不用重新inflate view了,提高了效率
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.
//rebind过程,如果缓存和child不一样,重新放回废弃队列
mRecycler.addScrapView(scrapView, position);
}
}
//设置layout参数
setItemViewLayoutParams(child, position);
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}
到此为止,layout流程已经分析完了,缓存间的数据流转也明了了。在layout之前,将所有的显示的子view添加到active缓存当中。然后删除所有的子view,重新加载。在重新加载过程中,会先到active缓存中查找数据,如果有就直接添加子view,如果没有再去废弃缓存中查找。最后子view添加完后,将active缓存中剩下的view添加到废弃缓存中。每次添加废弃缓存时,都要去检查废弃缓存的容量,如果超过了active缓存的长度,则从队尾开始删除,直接少于为止。
4、滑动原理
Listview在layout过程中,只会在可见区域添加子view,可见区域外并不会添加,而滑动过程中用户能够看到新的子view,那么滑动时肯定动态添加子view了。另外缓存数据肯定也有一些操作。带着这些猜想一起来看吧。
滑动的流程稍显复杂,先来一张流程图:
猜想都在 trackMotionScroll 这个方法中,删除不可见子view并缓存起来,然后添加新的子view,最后再将子view的位置也移动,尽在于此。
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
//判定滑动方向,如果 incrementalDeltaY 小于0,则是向下滑动
final boolean down = incrementalDeltaY < 0;
int start = 0;
int count = 0;
if (down) {
int top = -incrementalDeltaY;
for (int i = 0; i < childCount; i++) {
//向下滑动,子view的bottom值大于listview的top值,则表示子view仍居于可视位置,那么这类子view略过不处理
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
//将不可见view添加到废弃缓存中
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
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) {
mRecycler.addScrapView(child, position);
}
}
}
}
//count代表要删除的子view,如果count大于0,则从start开始,删除count个子view,这些子view已经不可见了
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
//滑动剩余子view,让子view也跟着手指动起来
offsetChildrenTopAndBottom(incrementalDeltaY);
//有些子view不可见了,必然有空位出来,空位要添加新的子view,fillGap,听名字就是填充子view的方法
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
}
读完代码后,上面判断down和up是应该相反的方法,如果是向上滑动,则是走down的流程,向下则是up的流程。
在向上滑动过程中,从第一个子view开始计算,如果子view的bottom位置小于listview的top位置,显然这个子view已经不可见了,要被添加到废弃缓存中去。最后记录起点,从起点开始删除count个子view。再执行 offsetChildrenTopAndBottom 方法,移动仍然可见的子view。
public void offsetChildrenTopAndBottom(int offset) {
final int count = mChildrenCount;
final View[] children = mChildren;
boolean invalidate = false;
for (int i = 0; i < count; i++) {
final View v = children[i];
v.mTop += offset;
v.mBottom += offset;
if (v.mRenderNode != null) {
invalidate = true;
v.mRenderNode.offsetTopAndBottom(offset);
}
}
if (invalidate) {
invalidateViewProperty(false, false);
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
从代码中可知,其实是遍历所有可见子view,直接将子view的top值变化,这样子view的位置当然就变了。于是listview可以跟手滑动了。
5、总结
Listview还有一处值得讨论,就是它的adapter设计。Listview在设计之初,肯定是想能显示一切类型的数据,它的子view可以是各种各样的格式。既然子view的格式千差万别,那么如何来兼容这些不同的数据,不同的格式。
adapter应运而生,将数据和view样式放到adapter中让用户自己定义,这实在是天才的设计,这样解耦了数据和listview的内在逻辑,listview得以专心处理自己的功能,而不用在意数据问题了,用户也能有更大的定制空间了。
listview整体代码还是非常复杂,本文之后,获益良多,多读源码,越读越会有新的收获。