单类型View缓存机制
- 请看上图,字母前面的数字表示元素在屏幕上的position,字母代表着View的类型,例如
1A
就表示第一个位置并且类型为A的View 当从状态一转变为状态2后,1A被滑出,5A被滑入,由于5A在被滑入之前缓存池中没有元素,所以5A将被创建,并且当1A滑出屏幕时将被投入到缓存池中,缓存池是一个ArrayList的数组
private ArrayList<View>[] mScrapViews;
,我用下图代替
当从状态2变为状态3时,缓存池的状态如下图所示,注意,这里的6A其实复用的是缓存池中存在的1A,6A滑入时发现缓存池中有和自己类型相同的View,则直接将1A从缓存池中取出,相似的,2A被滑出后将被缓存池回收。
结论:有没有一种生产者和消费者的感觉,一只手从缓存池中拿缓存,相应的一只手把移除的View投入缓存池中,这样会进入一个良性循环中,既无论ListView需要展现多少数据,在内存中存在的View的数量是恒定的,一直为一个屏幕所能展现的所有View+1(缓存池中的View),注意这个前提是基于加载的都是同类型的情况。
多类型View缓存机制
多类型View的缓存机制和上面相差不大,请看下图
说一下状态4和状态5,从状态3 -> 状态4时,缓存池缓存了离开屏幕的3C,并且滑入7A时系统发现缓存池冲存在A类型的View,所以7A直接复用了1A,当从状态4 -> 状态5时,由于4C滑出,自然而然被缓存,注意此时缓存池中以及有了两个C(这就是为什么缓存池使用的数据结构是ArrayList数组的原因,也符合生产者和消费者的规则)
缓存原理剖析
- 不知道大家有没有这样的一个疑惑,负责缓存的mScrapViews数组的容量是谁来确定的?并且,同类型的View被缓存时投入了同一个ArrayList中又是如何实现的?
- 当我们需要ListView支持多类型复用时,往往要覆盖这两个方法(以下是他们的默认实现),
getViewTypeCount
就决定了mScrapViews数组的长度,getItemViewType
就决定了相同类型的View投放到哪个坐标下,这句话的意思就是相同类型的View需要返回相同的值,并且它的值必须是从0开始依次递增的。,因此,我们由它们俩的默认实现看出当我们使用同类型加载数据的ListView时,这两个方法我们不必去理会。
public int getItemViewType(int position) {
return 0;
}
public int getViewTypeCount() {
return 1;
}
ListView的缓存原理由它的父类AbsListView的内部类RecycleBin负责,我们接下来就依次研究其中的重要方法。
以下是缓存池的初始化,传递的参数既是
getViewTypeCount
的返回值
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中的元素第一次初始化都会调用此方法,通俗来讲它是给每个View分类编号,请重点关注这句代码
lp.viewType = mAdapter.getItemViewType(position);
,调用了我们会复写的getItemViewType(position)
方法,所以说,让相同类型的View返回同一个值是需要我们自己去维护的
private void setItemViewLayoutParams(View child, int position) {
final ViewGroup.LayoutParams vlp = child.getLayoutParams();
LayoutParams lp;
if (vlp == null) {
lp = (LayoutParams) generateDefaultLayoutParams();
} else if (!checkLayoutParams(vlp)) {
lp = (LayoutParams) generateLayoutParams(vlp);
} else {
lp = (LayoutParams) vlp;
}
if (mAdapterHasStableIds) {
lp.itemId = mAdapter.getItemId(position);
}
lp.viewType = mAdapter.getItemViewType(position);
if (lp != vlp) {
child.setLayoutParams(lp);
}
}
- 接下来就是ListView中最核心的方法,我将难点都用注释标注,应该很好理解
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
// 根据position调用getItemViewType(position)方法可以获得View在缓存池的位置
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
// 如果不为null,我们就可以利用convertView进行复用操作
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
// 如果返回的View和我们从缓存池中拿出的View不同,则把它重新存进去
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
// 当缓存池中没有时,传递convertView为null
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
- 以上就是ListView缓存原理的分析,大家可以进入源代码的世界享受巧妙编码带来的享受,加深自己的理解感触。
总结
- ListView的缓存机制利用了生产者和消费者的原理,View滑出屏幕时把它缓存起来,当下一个View滑入时,如果能在缓存池中找到,则把它取出来(从缓存池中remove掉了)复用。
ListView缓存机制可以优化的地方
- 用于缓存的每一个类型的ArrayList没有容量限制,有可能内存中会缓存了很多同类型的View,这样是很大的浪费,解决方案是可以设置最大缓存数量,比如当A类型的View超过10个时,直接干掉ArrayList首部的View或者不再向缓存池中加入此类型的View,直到此类型的View数量在缓存池中小于5
- 当同个类型的View的多个实例(数量大于某个值)在缓存池中存在过长时间时(时间大于某个值),可以利用缓存过期原理,逐步remove掉arraylist中的数据,并动态的设置变换缓存时间,达到高效利用(比如当arraylist[0]中存在11个缓存的View时,并且时间已经过了第一次过期时间5min,那么将remove掉第11个元素,如果又过4分钟此arraylist[0]还没有元素被使用,remove掉第10个元素,依次类推,直到最后一分钟还未使用,直接removeAll)