1、问题引出
项目中使用的listview拥有多个不同类型的item,而且有些item数量只有一个,之前以为滑动时view无法复用。
2、分析
说到ListView的View复用,先来一张经典的原理解析图。
从图中可以看出,当listview某一item完全滑出屏幕时,内部的一个Recycler容器会缓存这个item。当listview新的一项需要在屏幕上展示时,这项缓存的item会在getView方法中以convertView参数传入,从而达到复用的效果,同时更新Recycler中的缓存item。通常是这么用的:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyLayout layout = convertView instanceof MyLayout ? (MyLayout) convertView : null;
if (layout == null) {
layout = (layout) res.inflate(getContext(), R.layout.my_rank_layout, parent, false);
}
layout.setData(data);
return layout;
}
但是,上面这种说法并不是完全正确的。这里需要注意一个问题,就是item的viewType。其实仔细看上面的原理图中的Recycler部分也可以发现,Recycler会分别缓存不同viewType的item。查看AbsListView源码中Recycler实现:
// item缓存数组
private ArrayList<View>[] mScrapViews;
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;
}
listview的setAdapter方法中会调用该方法,从代码可以看出,mScrapViews数组大小为adapter的viewTypeCount,数组每个元素又是一个list,即可以为相同viewType的item缓存多份。当listview滚动时,会将滑出屏幕的item缓存至Recycler。部分源码如下:
private void scrollListItemsBy(int amount) {
...
final AbsListView.RecycleBin recycleBin = mRecycler;
if (amount < 0) {
...
// listview上拉时,上面的item需要缓存
View first = getChildAt(0);
while (first.getBottom() < listTop) {
AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(first, mFirstPosition);
}
detachViewFromParent(first);
first = getChildAt(0);
mFirstPosition++;
}
} else {
...
// listview下拉,下面的item需要缓存
int lastIndex = getChildCount() - 1;
View last = getChildAt(lastIndex);
while (last.getTop() > listBottom) {
AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(last, mFirstPosition+lastIndex);
}
detachViewFromParent(last);
last = getChildAt(--lastIndex);
}
}
}
当adapter的getView回调时,会从Recycler中去取符合条件的item缓存。Recycler的getScrapView()方法会根据当前item的viewType去寻找合适的缓存,代码如下:
/**
* @return A view from the ScrapViews collection.
*/
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;
}
3、结论
listview内部会分别缓存不同类型的item。
续:
(1)为什么每个viewType都会用一个ArrayList来缓存,也就是说为什么会缓存多份相同viewType的item?
根据上面的原理图,ListView滑动时上方滑出屏幕的item进入缓存,当下方有相同viewType的item滑进屏幕时,应该可以直接去缓存中取到缓存,这不是刚好一进一出,用一个变量去缓存不就可以了?其实不然,考虑ListView第一屏显示的都是相同viewType的item,第二屏都是跟第一屏不一样类型的item,那么当第二屏滑进屏幕时,第一屏的item都会进入缓存,并且无法被消费,这就要求针对每个类型的item都需要一个List来缓存多份。
(2)既然每个viewType的item都会缓存多份,那么问题又来了,最多可以缓存多少份呢?有没有数量限制,或者说可以无限缓存呢?
通过阅读源码,根本没有看到关于缓存数量限制的代码,那么真的是可以无限缓存吗?那内存不是爆了吗?其实不是的,仔细想想就能明白,针对每种类型的item,最多只会缓存一屏能容下该类型item的数量。因为超出屏幕的相同类型item滑进屏幕时,就会消费掉缓存,所以源码中根本无需做缓存数量的限制,RecycleBin机制本身就能够达到一种动态的平衡。