处理ListView过程中用到的缓存
mActiveViews | new View[0];可见view的数组 |
mScrapViews | ArrayList |
mCurrentScrap | ArrayList |
在调用setAdapter的时候,首先会调用setViewTypeCount来初始化上述几个重要的集合
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//根据viewType初始化mScrapViews
ArrayList[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList();
}
mViewTypeCount = viewTypeCount;
//mScrapViews第一个元素
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
ListView最终继承View,那么在View绘制的过程分三步:onMeasure测量view的大小,onLayout确定view的布局,onDraw将view绘制到界面上。其中ListView中重点在onLayout中,其中实现是在父类AbsListView中:
/**
* Subclasses should NOT override this method but
* {@link #layoutChildren()} instead.
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
//重点来了,进行子元素的绘制,实现过程是在ListView中
layoutChildren();
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
mInLayout = false;
}
其中重点在于layoutChildren(),主要实现在ListView中,实现子元素的绘制,下面重点介绍。另外我们知道View从初始化到显示到界面,都要经历两次onMeasure和onLayout,那么我们就分别介绍下这两次,layoutChildern都经历了些什么
1)layoutChildern()
此时ListView中没有任何子view,childCount = 0,并且mActiveViews和mScrapViews中也没有缓存任何view, 会根据mLayoutMode来决定执行第一次layout。默认的为LAYOUT_NORMAL,并且事件是从上往下,所以走到第一if,调用fillFromTop()来执行下面的逻辑。
@Override
protected void layoutChildren() {
...... //省略代码
try {
...... //省略代码
final int childCount = getChildCount();
...... //省略代码
switch (mLayoutMode) {
...... //省略代码
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
...... //省略代码
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
...... //省略代码
} finally {
...... //省略代码
}
}
2)fillFromTop()
进入到fillFromTop(),继续跟进到fillDown(),从fillDown()中可以看到最终调用到 makeAndAddView()来返回对应的View,那就直接进入到makeAndAddView()方法。
3)makeAndAddView()
在makeAndAddView()中,我们可以看到首先通过mDataChanged去看是否数据发生变化来决定下面的流程。此时因为第一次layout,所以此时该条件成立
然后去获取activeView,但是由于第一次layout,此时获取的肯定为null。所以执行obtainView()来获取该位置下的view
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
//数据没有发生变化
if (!mDataChanged) {
// 该缓存过activeView,则视图加载这个位置的view
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// 通过下面的方法返回viw
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
4)obtainView()
那直接进入到父类AbsListView下的obtainView()看下里面的逻辑:
首先获取transientView,这个跟我们讨论的无关,暂时忽略;
接着就是去获取缓存过的scapeView,由于第一次layout,此时获取的肯定为null;
View obtainView(int position, boolean[] outMetadata) {
......//省略代码
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
......//省略代码
return transientView;
}
//获取缓存过的scrapView
final View scrapView = mRecycler.getScrapView(position);
//根据scrapView通过getView方法创建view
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
// 下面的代码主要就是设置child的背景色(就是平时我们设置的ListView的这个属性mCacheColorHint)和布局参数(LayoutParams)
......//省略代码
return child;
}
那么就根据这个scapeView为null的传入adapter.getView创建对应的child。此时就调用到了我们自定义的Adapter的getView方法,也就是传入到getView中的convertView为null,按照常规的写法,此时convertView为null,则通过布局文件创建出该View。所以我们可以看到如果scapeView不为null,即我们缓存过scapeView时,此时传入的convertView不为null,也就会走我们在Adapter定义的直接从该convertView中取出将holder设置成tag的holder。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(onBindLayout(), parent, false);
holder = new ViewHolder();
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
......//省略代码
return convertView;
}
那么继续往下执行scapeView为null,剩下的代码就是设置该view的背景色和布局参数,所以则直接将创建的getView方法返回。
5)返回到makeAndAddView()看下面的代码
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
这里主要知道就是调用 addViewInLayout将4中获得的view添加到ListView中。
通过上述的5步就已经完成了第一次Layout,那么紧接着的第二次Layout来了哟
第二次的Layout仍然会继续从layoutChildern()开始。但相比较于第一次childCount不为0,那么相应的下面的逻辑也就发生变化了。
1)layoutChildern
此时childCount不为0,走到缓存view的过程,此时数据并没有发生变化,所以就将当前的view加入到mActiveViews中。是不是疑问这里缓存当前view的用处何在呢?其实下面的一行代码就清晰了。
@Override
protected void layoutChildren() {
...... //省略代码
try {
...... //省略代码
final int childCount = getChildCount();
...... //省略代码
// 根据数据是否发生变化,将view保存到对应的集合中,此时数据并没有发生变化
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
...... //省略代码
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//将之前添加的view从ListView中移除
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
...... //省略代码
default:
if (childCount == 0) {
...... //省略代码
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
...... //省略代码
} finally {
...... //省略代码
}
}
detachAllViewsFromParent();会将之前加入的view都移除,为了防止在渲染的时候,view重复,那么在后面在加载的时候会不会还需要重新通过布局文件来inflater进来呢?其实这就是mActiveViews这个集合的作用,就是不需要在重新inflater,直接将缓存到mActiveViews这个里面的view取出即可。
//将之前添加的view从ListView中移除
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
接着还说根据mLayoutMode为LAYOUT_NORMA,并且childCount不为0,走到else之中,我们可以看到最终调用到fillSpecific()方法。
2)fillSpecific()
会创建出当前position的view,然后根据mStackFromBottom分别创建出前一个和后一个的view,最后根据不同的情况返回对应的位置的view,最终还是调用到makeAndAddView()来创建该view
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
//首先是当前position的view
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
//然后加载该view下的前一个view和后一个view
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) {
above = fillUp(position - 1, temp.getTop() - dividerHeight);
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown();
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
//最后根据不同的情况返回对应的view
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
3)makeAndAddView()
这次调用的makeAndAddView()和第一次layout已经不一样了,此时可以取到对应位置的activeView,直接通过setupChild()将activeView加到ListView中
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
//数据没有发生变化
if (!mDataChanged) {
// 该缓存过activeView,则视图加载这个位置的view
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
......//省略代码
}
此时经过上述的 第一次和第二次Layout,该ListView第一屏的数据已经显示出来了。我们也可以将mActiveView理解成ListView的一个缓存,就是在在第二次layout时候,临时保存下当前显示的view,方便最后在显示的时候,可以直接把之前创建的view显示出来
当用户进行滑动显示ListView的更多数据的时候,这个地方就涉及到了ListView的另外一个缓存数组mScrapViews。这里当然要去看下ListView的onTouchEvent()事件。
我们主要看下在滑动的过程中,当前的view是怎么加入到RecycleBin中,新的View是怎么创建的
1)onTouchEvent()
在onTouchEvent()中看下ACTION_MOVE事件,跟踪到onTouchMove()事件中
2)onTouchMove()
若数据源发生变化的时候,则重新去layoutChildern,此时我们先不去考虑数据源发生变化的情况。手指在界面上滑动相当于相当于TouchMode是TOUCH_MODE_SCROLL,跟踪代码进入到scrollIfNeeded()中。
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
......//省略代码
//如果数据源发生变化,则调用该方法
if (mDataChanged) {
// Re-sync everything if data has been changed
// since the scroll operation can query the adapter.
layoutChildren();
}
......//省略代码
switch (mTouchMode) {
......//省略代码
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}
3)scrollIfNeeded()
该方法在手指滑动的时候就会调用到,所以这个方法会被调用多次。根据匹配 TOUCH_MODE_SCROLL该模式,进入到第一个if条件中。只要有滑动,incrementalDeltaY这个值就不为0,那么就会调用到trackMotionScroll()中。
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
......//省略代码
//匹配改种模式
if (mTouchMode == TOUCH_MODE_SCROLL) {
......//省略代码
// 只要incrementalDeltaY有滑动,则这个就有值,则调用到下面的这个方法里面
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
......//省略代码
}
}
}
4)trackMotionScroll()
首先会根据down滑动的方向,根据该子view的bottom如果已经小于top,说明该子view已经从顶部移除屏幕,然后将该view到mScrapViews中,若是从下往上,则从0开始加进去,如果是从上往下,则从最后一个开始加入;
然后调用detachViewsFromParent,将所有的缓存后的view从屏幕中移除;
通过offsetChildrenTopAndBottom()将listview中所有的子view进行偏移 ;
接下来的代码就是判断是不是所有的第一屏的view已经移除屏幕,如果已经移除,则调用fillGap()来加载第二屏的数据。那进入到fillGap()方法中看看里面的逻辑。
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
......//省略代码
//根据down是从下往上滑,进入到下面流程中
if (down) {
......//省略代码
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
......//省略代码
}
......//省略代码
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
......//省略代码
offsetChildrenTopAndBottom(incrementalDeltaY);
......//省略代码
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
......//省略代码
}
5)fillGap()
最终实现在ListView中,显然就是根据down来确定调用fillDown()/fillUp(),那么最终调用到了makeAndAddView()方法。
6)makeAndAddView()
此时和前面的第一次layout的makeAndAddView()判断逻辑是一样的,此时的activeView是null,该集合里面的元素是在什么时候清空的呢?当然就是在getActiveView()方法里面,获得当前position的view之后,接着就将该position 下的view置为null。同理mScapeView也是一样的。
所以仍然回到obtainView()中获取view。
7)obtainView()
View obtainView(int position, boolean[] outMetadata) {
......//省略代码
//获取缓存过的scrapView
final View scrapView = mRecycler.getScrapView(position);
//根据scrapView通过getView方法创建view
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
......//省略代码
return child;
}
此时获取到scrapView不为null,那么传入到getView()中获取对应的child,此时这里就是我们在getView()中的convertView不为null的情况,通常做法就是从convertView中取出对应的tag,然后下面就是对holder中的控件进行赋值即可。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(onBindLayout(), parent, false);
holder = new ViewHolder();
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
......//省略代码
return convertView;
}
那么继续往下执行scapeView为null,剩下的代码就是设置该view的背景色和布局参数,所以将getView()得到的child返回。
8)返回到makeAndAddView()看下面的代码
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
在setupChild()主要知道就是调用 addViewInLayout将4中获得的view添加到ListView中。
1)layoutChildern()
相比较于两次layout,其中就是dataChange为true,会将View加载到mScapView中,剩下的逻辑同第二次layout。那么就进入到fillSpecific()方法
@Override
protected void layoutChildren() {
...... //省略代码
try {
...... //省略代码
final int childCount = getChildCount();
...... //省略代码
// 根据数据是否发生变化,将view保存到对应的集合中,此时数据并没有发生变化
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
...... //省略代码
}
//将之前添加的view从ListView中移除
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
...... //省略代码
default:
if (childCount == 0) {
...... //省略代码
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
...... //省略代码
} finally {
...... //省略代码
}
}
2)fillSpecific()
同第二次layout,那么进入到makeAndAddView()
3)makeAndAddView()
现在mDataChanged已经为true,所以进入到obtainView()
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
......// 省略代码
}
// Make a new view for this position, or convert an unused view if
// possible.
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
4)obtainView()
同ListView滑动过程中中的obtainView()。所以不在多余解释。
通过上面的几个过程的源码跟踪,我们可以大体了解ListView中缓存机制的处理过程:
1)在初始化显示第一屏的数据的时候,经过两次layout,第一次layout主要就是通过adapter的getView()中的inflater的布局文件创建出该item的view;在第二次layout的时候,首先会将之前创建的view放到mActiveView中暂时缓存下,然后将所有的View移除,在将mActiveView中的view加载到ListView中,这样第一屏的数据就显示出来了。注意当从mActiveView取出view的时候,也会将该集合中的view给清空。
2)当向上滑动时,则首先会将移除屏幕的view缓存到mScapView中,在滑动的过程中,依次又会将mScapView取出View加载到ListView中