文章目录
- ListView缓存机制小结
- 前言
- 概述
- layout过程
- 第一次Layout
- 第二次Layout
- 滑动事件
- 参考资料
ListView缓存机制小结
前言
概述
ListView的缓存通过父类AbsListView中的内部类RecycleBin
实现,这个类中有两级缓存:ActiveViews
和ScrapViews
- ActiveViews用来缓存滑动后还留在屏幕内的itemview,供layout过程使用
- ScrapViews用来缓存滑出ListView的itemview,供onTouchEvent方法使用
RecycleBin
可以看到,mScrapViews缓存是一个ArrayList类型的数组,数组的长度代表itemview的类型个数,每个索引的ArrayList的长度表示缓存的滑出屏幕的同类型itemview的个数。
class RecycleBin {
private View[] mActiveViews = new View[0]; //一级缓存,用来缓存ListView第一次layout过程的itemview,在第二次layout过程会被降级到mScrapViews中
private ArrayList<View>[] mScrapViews; //二级缓存,缓存已经滑出ListView的itemview,可以被adapter作为convertview进行重用
}
mScrapViews的初始化
//AbsListView
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; //这里的viewTypeCount就是我们通过继承BaseAdapter重写其中getViewTypeCount方法指定的
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0]; //
mScrapViews = scrapViews; //将上面创建的scrapViews赋给mScrapViews
}
mScrapViews的缓存大小就是getViewTypeCount方法返回值指定的大小。
在加载多子布局的情况就需要重写ListView的getItemViewType和getViewTypeCount方法;
另外,无论setViewTypeCount()/getViewTypeCount()值为几,RecycleBin都只有一个,而不会单独另起一个。唯一区别的是,当值为1时,即只有一种viewtype时,RecycleBin使用的是ArrayList<View> mCurrentScrap
来回收和存储滚出屏幕的views,当大于1时,RecycleBin用的是ArrayList<View>[] mScrapViews
,
layout过程
由于父ViewGroup的原因可能导致ListView layout多次。
ListView主要看onLayout方法,因为ListView的大小就是我们指定的大小,所以onMeasure方法就没什么看的了;又因为ListView的绘制都是交给子项来完成的,所以onDraw方法也没有了解的意义了。
第一次Layout
第一次Layout,这时还没有item子项的缓存,即RecycleBin中的mActiveViews数组大小为0,mScrapViews还未初始化,所以会通过Adapter的getView方法来获取子项的布局。
调用链
onLayout()->
layoutChildren()->
fillFromTop()->
fillDown()-> //在一个while循环中调用makeAndAddView方法获取子布局的view,直到占满整个屏幕
makeAndAddView()->
obtainView()-> mAdapter.getView()->
setupChild() //将getView方法返回的view添加到ListView中
makeAndAddView()
这个方法很重要,它体现了ListView缓存的特点:
- 先从RecycleBin的ActiveView获取缓存
- 再从RecycleBin的ScrapView获取缓存
- 最后考虑从Adapter的getView方法加载子布局
因为getView方法中是通过inflater的inflate方法来加载子布局的,这样获取效率较低,所以ListView会优先从两个缓存中获取。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
//这里通过getActiveView方法从RecycleBin的ActiveView中获取
final View activeView = mRecycler.getActiveView中获取(position);
if (activeView != null) { //第一次还没有缓存,所以这里为null
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
//从scrapview缓存获取或加载一个新的itemview到此位置
final View child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView()
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
//再从RecycleBin的scrapview中获取屏幕外item的缓存,因为第一次layout,所以没有缓存
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) { //这里为null
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this); //通过关联的Adapter的getView方法获取item的view
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
第二次Layout
第二次Layout的流程和第一次差不多。
onLayout()->
layoutChildren()-> //会将第一次layout添加到ListView中的view全部添加到RecycleBin.mActiveViews中,然后通过detachAllViewsFromParent()将所有的itemview从ViewGroup中先移除,避免后面重复添加。
fillSpecific()->
makeAndAddView()->
setupChild() //通过attachViewToParent()将一个之前detach的itemView重新attach到ViewGroup上
这次makeAndAddView方法中进入了不同的分支:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
//这里通过getActiveView方法从RecycleBin的ActiveView中获取
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) { //前面将所有itemview加入到了RecycleBin的mActiveView中,所以这里不为null
setupChild(activeView, position, y, flow, childrenLeft, selected, true); //其中会通过attachViewToParent()将一个之前detach的itemView重新attach到ViewGroup上
return activeView;
}
}
final View child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
滑动事件
滑动时mActiveViews缓存了滑动后还在屏幕内的itemview,如上面layout过程所分析。下面onTouchEvent方法只会使用mScrapViews屏幕外itemview的缓存。
调用链
onTouchEvent()->
trackMotionScroll()-> //如果该子View已经移出屏幕了,就会调用RecycleBin.addScrapView()将这个View加入到mScrapViews缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。还会调用detachViewsFromParent()将移出屏幕的itemView全部detach掉
fillGap()->
fillUp()/fillDown()->
makeAndAddView()->
obtainView()
因为在第二次layout过程中通过getActiveView获取过ActiveView中的itemview,所以在makeAndAddView()中就获取不到它们了,这个原因在 getActiveView() 中:
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; //可以看到,这里会将找到的itemview缓存置空
return match;
}
return null;
}
所以下面会到obtainView获取屏幕外item的缓存或从Adapter的getView方法中加载。
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) { //addScrapView()方法将移出屏幕的View加入到了mScrapViews缓存当中,所以这里不为null
child = mAdapter.getView(position, scrapView, this); //可以看到,第二个参数为获取到的scrapView,这就是我们熟悉的View类型的convertview!
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
上面obtainView方法中 child = mAdapter.getView(position, scrapView, this); 这行代码就解释了我们为什么要在Adapter的getView方法对第二个convertview做判断了,这样可以复用相同类型的itemview,避免通过inflate方法加载以提高效率。
顺便看看上面 mRecycler.getScrapView(position) 怎样获取ScrapView缓存:
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position); //通过Adapter.getItemViewType方法获取itemview的类型,它同时是该类型ArrayList在mScrapViews这个ArrayList数组中的索引
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) { //只有一种itemview的类型就在mCurrentScrap中获取缓冲
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) { //否则就在该类型的ArrayList中获取缓存
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
//从后往前遍历是为了找到最近使用的itemview缓存
for (int i = size - 1; i >= 0; 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);
clearScrapForRebind(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearScrapForRebind(scrap);
return scrap;
} else {
return null;
}
}