由于ListView使用懒加载的机制,只加载当前屏幕中可见条目的视图,处于不可见的条目是不会被加载的。在动态滑动过程中,屏幕的
可见元素不断的发生变化,需要不断的创建需要显示在当前屏幕中的条目元素,而通常创建条目view集合的方法为inflate xml文件,这是
一个比较耗时的操作。所以谷歌的工程师使用了条目convertView的缓存机制,缓存机制的实现类为RecycleBin,它是作为AbsListView的内
部类存在的。分析完RecycleBin,我们基本上就掌握了缓存机制的原理。
一.二级缓存池的初始化
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;
}
这个方法为初始化缓存池。
缓存池其实就是一个ArrayList<View>。
scrapViews 缓冲池集合,这是一个数组,数组的每一个元素为ArrayList<View>。所以缓存池不只一个,缓存池的个数为viewTypeCount
这个方法只有一个地方调用,即ListView的setAdapter方法中,如下:
public void setAdapter(ListAdapter adapter) {
............
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
}
缓存池集合在整个adapter的生命周期中size是不会改变的,adapter.notifyDatasetChanged不会改变缓存池集合的size.
mAdapter.getViewTypeCount()的返回值必须唯一,如果在setAdaper中返回值为2,adapter.notifyDatasetChanged之后返回值改为3,
这时数据确实发生了改变,则必然会抛出数组下标越界异常,具体抛出在layoutChildren()方法中的
recycleBin.addScrapView(getChildAt(i));,因为add缓冲池方法不会检查数组角标。
二.二级缓存池的清理
void clear() {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
final int scrapCount = scrap.size();
for (int i = 0; i < scrapCount; i++) {
removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
}
} else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
}
}
}
}
清空缓存池集合
默认情况下,mViewTypeCount为1,缓冲池为mCurrentScrap。如果重写了adapter的getViewTypeCount,则所有缓存池都会清空。
三.从二级缓存池查找元素
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;
}
根据position从缓存池查找对应的缓存itemView。查找时先找缓存池,然后再从缓存池中取出最后一个元素。
查找对应的缓存池是根据scrapViews数组的下标直接映射的。下标是通过mAdapter.getItemViewType(position)
取得的。所以重写getItemViewType方法时, type的返回值必须在[0.....getViewTypeCount]之间,其他的type
值将导致某些类型的条目不被复用,因为type的值不在数组下标范围内,不能取出相应的缓存池。
四.添加元素到二级缓存池
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)) {
return;
}
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
将某一个条目添加到缓存池中。
添加时先找viewType,在找到对应的缓存池添加。
这个地方没有做数组下标越界检查,如果type值大于缓存池集合的大小,则会抛出异常。
五.一级缓存池分析
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 != AdapterView.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;
}
}
}
当前屏幕中的视图缓存集合。
mActiveViews为view的数组,只保存当前屏幕中的视图条目。
这个方法在layoutChildren中(dataChanged == false)时调用。recycleBin.fillActiveViews(childCount, firstPosition);
如果adaper中的数据没有发生改变,则ListView在layout时会保存所有当前屏幕中的元素。以备下次界面更新时使用。
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视图。如果较上一次比较,adapter中数据没有发生改变,在创建itemView时,会优先从activeViews池中找
是否存在已有的条目view,如果有则直接使用,连setData的过程都没有了。如下:
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,
position, getChildCount());
}
// 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 = obtainVew(position);
由于setupChild方法没有调用adaper.getview方法,所以这种情况下根本就没有调用getview的重写方法。
obtainVew(position);这种方法生成的view是调用getView方法的。
总结:
1.ListView存在两个缓存池系统,一级缓存复用时不需要调用getView,二级缓存需要调用getView.
2.ListView的缓存并不是像某些博客上面写的循环利用图那样简单,向下滑动时,上面的Item被下面的再次使用,这是一种误导。
实际情况是一个缓存堆栈,后进先出,上面消失的item不一定就是被下面的再次使用了,下面要使用的只是缓存堆栈的最后一个,
和上面消失的没有直接联系。
3.ListView的二级缓存可以缓存不同类别的View。即使某个View被滑出屏幕很远了,回来时仍然可以复用。