文章目录

  • ListView缓存机制小结
  • 前言
  • 概述
  • layout过程
  • 第一次Layout
  • 第二次Layout
  • 滑动事件
  • 参考资料


ListView缓存机制小结

前言

概述

ListView的缓存通过父类AbsListView中的内部类RecycleBin实现,这个类中有两级缓存:ActiveViewsScrapViews

  • 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缓存的特点:

  1. 先从RecycleBin的ActiveView获取缓存
  2. 再从RecycleBin的ScrapView获取缓存
  3. 最后考虑从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;
    }
}