由于这些东西比较容易忘记,记录一下,方便以后查看
ListView能够展示成百上千条数据都不会OOM,是因为使用了RecycleBin机制来复用View,所以Listview内部添加的View也就屏幕那几个
当使用ListView.setAdapter()给它设置数据适配器的时候,就会调用requestLayout()来绘制出需要显示的内容.
重点是在onLayout()这个过程,所以就从这里的源码看起了,onLayout在ListView的父类AbsListView中实现
ListView至少会调用两次onLayout
第一次onLayout
@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();
}
//重点代码在这里面
layoutChildren();
mInLayout = false;
...
}
}
onLayout会调用layoutChildren()来实现,AbsListView的layoutChildren是空方法,所以看ListView的该方法实现.
@Override
protected void layoutChildren() {
...
try {
super.layoutChildren();
invalidate();
...
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//移除所有的view,下面会重新添加,避免重复添加相同的view
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
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 {
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;
}
...
}
删除了很多代码,这里只看一些重点代码,看多了犯困,一开始会判断dataChanged.默认是false,当数据改变的时候的true,所以会调用recycleBin.fillActiveViews(childCount, firstPosition)方法
recycleBin.fillActiveViews()
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
这个时候childCount是0 ,所以这个方法暂时不用管,退出方法,返回到layoutChildren()方法当中继续,接下来会switch (mLayoutMode)
,判断一下,会进入default里面,这个时候childCount == 0
,然后发现里面还有一个判断mStackFromBottom,并且会调用fillFromTop
或者fillUp
方法,这两个方法都是去创建ListView的子View,并且显示出来
这里会调用fillFromTop(),fillFromTop方法再调用fillDown()方法
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;
}
//是否超过了ListView的高度,或者超过item数量了
while (nextTop < end && pos < mItemCount) {
// 判断是否是选择的position
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;
}
这里有个while 循环,调用makeAndAddView创建ListView的子View
看下makeAndAddView()
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
//如果有可用的view,直接添加并且返回
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
//这里必须返回一个view
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
先是进入mRecycler.getActiveView(position)
尝试从可用view数组mActiveViews里面获取,这个数组是在上面fillActiveViews()方法里面赋值的,因为刚刚调用fillActiveViews的时候childCount==0,所以这里是null,然后调用obtainView()方法,得到子View
obtainView() 在父类AbsListView实现
View obtainView(int position, boolean[] isScrap) {
...
final View scrapView = mRecycler.getScrapView(position);
//调用mAdapter.getView方法
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
...
return child;
}
先调用mRecycler.getScrapView
从废弃的View缓存中拿到scrapView,这个时候是null,然后就到了我们平时熟悉的地方mAdapter.getView(...)方法来创建view,并且返回出去,调用setupChild
添加到 ListView里面
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView==null){
convertView = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
viewHolder = new ViewHolder();
viewHolder.textView1=(TextView) convertView.findViewById(R.id.textView1);
convertView.setTag(viewHolder);
}
else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView1.setText(data.get(position).getContent);
return convertView;
}
所以我们平时要判断if(convertView==null)
来判inflate断创建还是复用
小结:
private View fillDown(int pos, int nextTop) {
//是否超过了ListView的高度,或者超过item数量了
while (nextTop < end && pos < mItemCount) {
// 判断是否是选择的position
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
...
return selectedView;
}
就算Adapter有很多数据,在fillDown()方法中,有个while循环判断while (nextTop < end && pos < mItemCount)
,限制着ListView一次只会创建屏幕能显示的子View个数,保证ListView中的内容能够迅速展示到屏幕上
第一次Layout过程结束
第二次onLayout
第二次Layout和第一次Layout的基本流程是差不多的,从layoutChildren()方法开始看起:
@Override
protected void layoutChildren() {
...
try {
super.layoutChildren();
invalidate();
...
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//移除所有的view,下面会重新添加,避免重复添加相同的view
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
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 {
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;
}
...
}
同样的会调用recycleBin.fillActiveViews(childCount, firstPosition)方法:
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
这里和上面第一次不一样了,这个时候childCount是有数量的,所以会把ListView的子View缓存到mActiveViews[ ]
数组里面,后面将会用到.
然后会调用一个非常重要的方法detachAllViewsFromParent(),这个方法会把 所有ListView中的子View全部清除,保证第二次Layout过程不会产生一份重复的数据.
然后同样的进入到switch的default里面,但是这次childCount不为0了,进入else然后调用fillSpecific()
方法,然后fillSpecific()里面其实还是调用fillUp()
或者fillDown()
方法,并且最后都是调用makeAndAddView()
来实现创建View
所以主要看makeAndAddView()
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
//如果有可用的view,直接添加并且返回
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
//这里必须返回一个view
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
这里也和第一次不一样了,由于在上面detachAllViewsFromParent
清空ListView子View之前,已经把子View缓存到RecycleBin的mActiveViews[ ]数组里面,所以这次child = mRecycler.getActiveView(position);
就能直接得到之前的view,然后再次添加并且返回
经历了这样一次detach又attach的过程,ListView中所有的子View又都可以正常显示出来了
那么第二次Layout过程结束
到这里ListViw就能正常的显示一屏幕的数据了,但是还有很多数据没显示出来的怎么办?所以要继续查看滑动事件的源码:
AbsListView的onTouchEvent()
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
onTouchDown(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
case MotionEvent.ACTION_UP: {
onTouchUp(ev);
break;
}
...
return true;
}
删除了很多代码,这里只关注ACTION_MOVE
事件,所以这里会调用onTouchMove()
方法,然后内部调用scrollIfNeeded()
,最后会调用到trackMotionScroll()
方法,只要在屏幕上稍微有一点点移动,这个方法就会被调用,所以滑动的时候会被调用多次
trackMotionScroll()
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
...
//判断滚动方式 上/下
final boolean down = incrementalDeltaY < 0;
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
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) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
int bottom = getHeight() - incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
bottom -= listPadding.bottom;
}
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
}
...
//如果计数器 > 0,就删除滚出屏幕的view
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
//移动ListView的内容
offsetChildrenTopAndBottom(incrementalDeltaY);
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
...
return false;
}
deltaY
表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY
则表示据上次触发event事件手指在Y方向上位置的改变量,所以可以根据incrementalDeltaY的正负来判断往上滚还是往下滚..
这里就拿down为true来看,这里会循环判断ListView的childCount,如果child.getBottom() >= top,就跳过,否则即是子View的bottom比ListView的top还小(也就是手指往上滑的时候,子View也会往上移动,当往上滚出屏幕的时候)就使用到RecycleBin机制的mRecycler.addScrapView(child, position)
把这个View添加到弃用的数组里面缓存起来
然后判断if (count > 0)也就是如果有滚出屏幕的view,就调用detachViewsFromParent()
把这个view从ListView里面删除掉
然后调用ViewGroup
的offsetChildrenTopAndBottom()方法来移动相应的偏移量,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果
ViewGroup的offsetChildrenTopAndBottom()方法
public void offsetChildrenTopAndBottom(int offset) {
final int count = mChildrenCount;
final View[] children = mChildren;
boolean invalidate = false;
for (int i = 0; i < count; i++) {
final View v = children[i];
v.mTop += offset;
v.mBottom += offset;
if (v.mRenderNode != null) {
invalidate = true;
v.mRenderNode.offsetTopAndBottom(offset);
}
}
if (invalidate) {
invalidateViewProperty(false, false);
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
然后调用fillGap(down)
方法
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
int paddingTop = 0;
...
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
int paddingBottom = 0;
...
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
可以看到fillGap()
最后还是调用fillDown()或者fillUp()方法来实现,最终还是调用makeAndAddView()
方法来实现
再看一下makeAndAddView():
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
//如果有可用的view,直接添加并且返回
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
//这里必须返回一个view
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
由于上面调用过一次mRecycler.getActiveView()方法从mActiveViews[ ]中取出了可用的View,mActiveViews每取出一个View就会把对应的View=null,所以这里拿到的是null,走到obtainView()
方法里面
obtainView()
View obtainView(int position, boolean[] isScrap) {
...
final View scrapView = mRecycler.getScrapView(position);
//调用mAdapter.getView方法
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
...
return child;
}
这次调用mRecycler.getScrapView()就能从上面滚出屏幕,被保存在弃用的数组里面的View,来进行重新复用了
getScrapView()
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
//每取出一个弃用的View,就会把index置为null,所以之后保存一屏幕的数据
activeViews[index] = null;
return match;
}
return null;
}
ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加
参考:
https://blog.csdn.net/guolin_blog/article/details/44996879