在学习Android开发的过程中,大家应该或多或少的会接触到ListView这个控件。那么,大家有没有思考过,一个屏幕只能显示一定的item,每次下滑后,消失的item去哪里了?当再次回到原来的位置的时候,加载出来的item是重新获取的还是从缓存空间里提取的呢?接下来,我们通过分析RecycleBin机制来探寻ListView的缓存机制。
RecycleBin中有两个数组,mActiveViews和mScrapViews分别存储可以直接复用的view(处于可见状态的view)和间接复用的view(处于不可见状态的view)。当屏幕向下滑动的时候,顶部view不可见时,会将其回收至RecycleBin中的mScrapViews数组中进行保存,底部需要显示一个新的View时,会从mScrapViews中取出一个View传至convertView进行复用。
在ListView中,重写addView方法,当调用时,会抛出异常。ListView是一帧一帧绘制的,会经历measure->layout->draw方法。在ListView布局的时候,会调用layoutChildren方法绘制子View。当刚开始执行layout的时候,ListView的children是上一帧中需要绘制的view的集合,当layout执行完毕时,children变成当前帧需要绘制的子View的集合。
当一个view不可见时,首先会将该view移至RecycleBin。会根据数据是否发生变化调用不同的方法。如果数据发生变化,会将所有字View移至RecycleBin的mScrapView数组中进行保存,数据未发生改变在将当前View放入mActiveViews数组中。
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
然后会将不可见的View清楚,调用detachAllViewFromParent()方法,将该view设置为null。
protected void detachAllViewsFromParent() {
final int count = mChildrenCount;
if (count <= 0) {
return;
}
final View[] children = mChildren;
mChildrenCount = 0;
for (int i = count - 1; i >= 0; i--) {
children[i].mParent = null;
children[i] = null;
}
当需要将一个view从RecycleBin中传至ListView时,会根据mLayoutMode的不同情况调用fillUp,fillDown等方法。
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
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 {
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;
我们以fillDown方法为例子,该方法传入两个参数,一个是view对应adapter数据源中的位置,和下一个view在ListView中的起点位置。当nextTop小于ListView的高度并且pos小于数据源的数据总数时,将当前view添加进ListView并改变nextTop和pos的数据,实现pos的自增和下一个view的起点。
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
接下来我们看一下makeAndAddView方法是怎么将view添加进ListView的
在makeAndAddView中,如果数据源没有发生改变,我们会首先通过getActiveView方法在mActiveViews中查找是否存在该View,如果不存在,通过调用obtainView方法从mScrapViews数组中查询是否存在。最后调用setupChild对viwe进行定位和量算。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}