ListView源码分析
Posted 花花young
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ListView源码分析相关的知识,希望对你有一定的参考价值。
前言
在android所有常用的原生控件当中,用法最复杂的应该就是ListView了,几乎所有的app都应用了列表来展示数据,并且能加载大量数据而不发生OOM。作为开发者有责任和义务去研究这个神秘的控件,接下来本文将为你揭开ListView的神秘面纱
开篇一问
ListView我们也用了很久,本文将结合以下问题进行深入:
- ListView为什么加载上百上千条数据而不发生OOM异常?
- Adapter的notifyDataSetChanged方法如何让ListView进行刷新的?
- ListView刷新显示Item列表的源码探索。
- ListView滑动情况Item列表的源码探索。
在开始之前我们来了解一下ListView的继承关系
对于ListView是用来显示列表,它不直接和数据源打交道,这才有了Adapter适配器做ListView和数据源的桥梁。可能你会问如果没有Adapter那也是可以的做的啊?但是你想过没有如果没有Adapter适配器那ListView做适配的工作就非常复杂了,因为数据源有多种类型所以会写多种相应的方法进行接收数据源,当有了新的类型数据源还需要在ListView源码里面去更改,这样会导致ListView的扩展性极差,超出了它本身应该承担的工作。OK,来张图体会一下:
问题一 ListView为什么加载上百上千条数据而不发生OOM异常?
ListView之所以没有发生OOM异常的原因在于内部引入了RecycleBin机制,另外我们需要说一点的是因为RecycleBin是AbsListView的内部类,所以继承AbsListView的GridView也具有RecycleBin机制。废话不多说,我们来了解一下RecycleBin的源码
class RecycleBin
private View[] mActiveViews = new View[0];
private ArrayList<View>[] mScrapViews;
private ArrayList<View> mCurrentScrap;
public void setViewTypeCount(int viewTypeCount)
.....
/**
* RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存 * 储到mActiveViews数组当中。
*/
void fillActiveViews(int childCount, int firstActivePosition)
......
/**
* 一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重 * 复利用。
*/
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;
return match;
return null;
/**
* 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调 * 用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View
*/
void addScrapView(View scrap, int position)
......
OK,RecycleBin源码分析到这,来做个总结
- RecycleBin有两个存储,活动状态mActiveViews和废弃状态mScrapViews
- mActiveViews用来存储屏幕显示的View,当划出屏幕的View将存入mScrapViews,新进来的View则先从mScrapView取,如果存在则将其置为活动状态
问题二 Adapter的notifyDataSetChanged方法如何让ListView进行刷新的?
notifyDataSetChanged通知ListView进行刷新其实内部使用到了观察者模式
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter
private final DataSetObservable mDataSetObservable = new DataSetObservable(); // 被观察者
public void registerDataSetObserver(DataSetObserver observer)
mDataSetObservable.registerObserver(observer);// 无非就是将Oberver存入protected final ArrayList<T> mObservers = new ArrayList<T>();
public void unregisterDataSetObserver(DataSetObserver observer)
mDataSetObservable.unregisterObserver(observer);
//更新数据
public void notifyDataSetChanged()
mDataSetObservable.notifyChanged();
这里有几点需要注意:
1. BaseAdapter为被观察者 ListView为观察者
2. registerDataSetObserver在setAdapter的时候就调用了此方法,DataSetObservable便持有了观察者对象,观察者存入了mObservers集合中,这也说明适配器可以接受多个ListView
3. notifyDataSetChanged用来通知观察者进行刷新数据
那我们来验证一下setAdapter源码验证当时的结论:
@Override
public void setAdapter(ListAdapter adapter)
if (mAdapter != null && mDataSetObserver != null)
mAdapter.unregisterDataSetObserver(mDataSetObserver);
if (mAdapter != null)
......
//将观察者传入到了Adapter中
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
那我们回来接着研究notifyDataSetChanged的源码,内部调用了mDataSetObservable.notifyChanged();
public class DataSetObservable extends Observable<DataSetObserver>
/**
* 唤醒每个观察者,当内容数据发生改变的时候将会调用
*/
public void notifyChanged()
synchronized(mObservers) // 一个适配器可以对应多个ListView,当进行刷新的时候会按顺序进行刷新防止发生错乱
for (int i = mObservers.size() - 1; i >= 0; i--)
mObservers.get(i).onChanged();
内部使用synchronized同步代码块用于防止多个ListView同时刷新同一个Adapter造成数据错乱的情况,接着就是用个循环遍历通知观察者刷新,OK,进入onChanged一探究竟:
class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver
@Override
public void onChanged()
super.onChanged();
if (mFastScroll != null)
mFastScroll.onSectionsChanged();
接下来又调用了super.onChanged()方法,在AdapterView内部类
class AdapterDataSetObserver extends DataSetObserver
private Parcelable mInstanceState = null;
@Override
public void onChanged()
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0)
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
else
rememberSyncState();
checkFocus();
requestLayout();
在onChange方法里面为一些变量赋了值,在这里最重要的还是requestLayout()方法通知ListView去调用onLayout方法对Item重新进行布局,也就是相应的刷新列表。
综上,我们从notifyDataSetChanged方法,一步步分析,最终通过调用ListView的onLayout方法进行刷新。在这里面我们了解到观察者模式
问题三 ListView刷新显示Item列表的源码探索
因为ListView继承的ViewGroup自然也离不开UI绘制的三大方法:onMeasure、onLayout、onDraw。在ListView当中,onMeasure()占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。所以说onLayout才是ListView的核心所在,并且在上面分析出来notifyDataSetChanged方法最终调用的是ListView的onLayout的方法,所以我们应该从ListView的onLayout方法开始分析:
/**
* 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();
layoutChildren();
......
当ListView的位置和大小发生改变时changed为true,则会遍历子View进行强制刷新;然而此次研究重点不在这里,继续往下看我们看到layoutChildren方法,用于对子View的排列布局,因为父类AbsListView layoutChildren是个空方法,所以实现在ListView中:
@Override
protected void layoutChildren()
try
super.layoutChildren();
invalidate();
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
......//这里主要是获取焦点代码,和此次分析无关
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) // 重点1
for (int i = 0; i < childCount; i++)
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
else
recycleBin.fillActiveViews(childCount, firstPosition);
// Clear out old views
detachAllViewsFromParent(); // 将所有的View取消父类的关联,目的是将View从ViewTree中拆分出来
recycleBin.removeSkippedScrap();
switch (mLayoutMode) //默认情况mLayoutMode为LAYOUT_NORMAL,所以直接执行default里面的代码
..... //其它的case代码
default: // 重点2
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;
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
// remove any header/footer that has been temp detached and not re-attached
removeUnusedFixedViews(mHeaderViewInfos);
removeUnusedFixedViews(mFooterViewInfos);
...... //设置ListView的selection
ok,来看一下我们的重点:
1、dataChanged只有当数据源发生改变的时候才会为true,所以第一次onLayout会执行fillActiveViews方法,因为刚开始的时候子View还未加载到ListView中,所以childCount=0,这个方法并没有什么作用。
2、默认情况mLayoutMode为LAYOUT_NORMAL所以会执行default代码块,又因为childCount=0并且默认的布局顺序是从上往下,因此会执行fillFromTop()方法
/**
* 填充ListView
*/
private View fillFromTop(int nextTop)
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0)
mFirstPosition = 0;
return fillDown(mFirstPosition, nextTop);
从mFirstPosition位置开始,自上而下开始去填充ListView,ok,进入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;
while (nextTop < end && pos < mItemCount) // mItemCount = getAdapter().getCount();
// 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;
一开始nextTop的值是第一个item顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值
所以退出循环有两种情况一种是mItemCount超出一屏且nextTop超出了当前屏幕;另一种是mItemCount不满一屏且nextTop没有超出当前屏幕。除了这里还调用了一个重要的方法makeAndAddView(),得到View并添加到ListView中:
/**
* Obtains the view and adds it to our list of children. The view can be
* made fresh, converted from an unused view, or used as is if it was in
* the recycle bin.
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected)
if (!mDataChanged)
// Try to use an existing view for this position.
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;
// Make a new view for this position, or convert an unused view if
// possible.
final View child = obtainView(position, mIsScrap); // 重点1
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); // 重点2
return child;
那我们来看重点1,第一次进来的时候mRecycler.getActiveView得到的是null,所以会执行obrainView方法生成View
View obtainView(int position, boolean[] outMetadata)
outMetadata[0] = false;
// 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)
.......
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
return transientView;
......
final View scrapView = mRecycler.getScrapView(position);
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();
setItemViewLayoutParams(child, position);
return child;
同样第一次的时候scrapView=null,所以开始的时候会调用mAdapter.getView(position,null,this),这种情况我们会自己inflate View,这里我们贴一下我们在使用Adapter的时候代码
class MyAdapter extends BaseAdapter
@Override
public int getCount()
return 0;
@Override
public Object getItem(int position)
return null;
@Override
public long getItemId(int position)
return 0;
@Override
public View getView(int position, View convertView, ViewGroup parent)
if(convertView == null)
//inflate view
else
//重用view
return null;
从源码也能看出来,当第一次进来的时候由于scrapView==null,所以我们在getView方法中converView参数就为null,需要重新inflate View。
OK,View得到了,那我们来看一下上面的重点2
/**
* Adds a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow)
......
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER))
attachViewToParent(child, flowDown ? -1 : 0, p);
// If the view was previously attached for a different position,
// then manually jump the drawables.
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position)
child.jumpDrawablesToCurrentState();
else
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)
p.recycledHeaderFooter = true;
addViewInLayout(child, flowDown ? -1 : 0, p, true);
// add view in layout will reset the RTL properties. We have to re-resolve them
child.resolveRtlPropertiesIfNeeded();
......//需要去测量View
第一次layout的时候isAttachedToWindow为false,所以会执行addViewInLayout方法
/**
* Adds a view during layout. This is useful if in your onLayout() method,
* you need to add more views (as does the list view for example).
*
* If index is negative, it means put it at the end of the list.
*/
protected boolean addViewInLayout(View child, int index, LayoutParams params,
boolean preventRequestLayout)
if (child == null)
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
child.mParent = null;
addViewInner(child, index, params, preventRequestLayout);
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
return true;
进入addViewInner方法
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout)
if (child.getParent() != null)
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
if (!checkLayoutParams(params))
params = generateLayoutParams(params);
if (preventRequestLayout)
child.mLayoutParams = params;
else
child.setLayoutParams(params);
addInArray(child, index);
// tell our children
if (preventRequestLayout)
child.assignParent(this);
else
child.mParent = this;
......
dispatchViewAdded(child); // 当添加View之后会进行回调onViewAdded方法
......
第一次layout就这样结束了。
但这样就结束了吗?对于一个简单的View界面显示都会经历至少两次onMeasure、onLayout方法,一次或者两次对我们开发者来说影响并不大,但是对于ListView却不同,ListView如果经过两次之后便会存在一份重复的数据,但是Google开发工程师巧妙的解决了这个问题,现在就让我们来分析一下它的巧妙之处吧,为了方便,这次我们从layoutChildren开始分析
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;
因为第二次layout,ItemCount不为0所以detachAllViewsFromParent()方法起作用了,这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。这里我们只看default代码,会调用fillSpecific
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*/
private View fillSpecific(int position, int top)
boolean tempIsSelected = position == mSelectedPosition;
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;
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
......
fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,OK,我们进入makeAndAddView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected)
if (!mDataChanged)
// Try to use an existing view for this position.
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;
// 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;
由于第二次layout,现在activeView!=null,所以将不再inflate View而是直接重用activeView
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow)
......
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER))
attachViewToParent(child, flowDown ? -1 : 0, p);
// If the view was previously attached for a different position,
// then manually jump the drawables.
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position)
child.jumpDrawablesToCurrentState();
else
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)
p.recycledHeaderFooter = true;
addViewInLayout(child, flowDown ? -1 : 0, p, true);
// add view in layout will reset the RTL properties. We have to re-resolve them
child.resolveRtlPropertiesIfNeeded();
...... // 测量代码
因为上面传入的isAttachedToWindow为true,所以会执行attachViewToParent方法。相比第一次layout的addViewInLayout的最大区别在于addViewInLayout是创建一个新View添加到ListView中,而attachViewToParent是将detach的View添加到ListView中,恰巧在layoutChildren方法中调用了detachAllViewsFromParent()将所有的子View都处于Detach状态。
经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。
问题四 ListView滑动情况Item列表的源码探索
上面我们分析的只不过当第一次进入的ListView如何加载Item的过程,并非ListView的核心代码,ListView的核心代码其实是在滑动的过程中,Item如何进行重用的。OK,我们来看AbsListView的onTouchEvent事件
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();
final int y = (int) ev.getY(pointerIndex);
switch (mTouchMode)
......
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
这里的事件TOUCH_MODE_SCROLL,所以会执行scrollIfNeed方法,进而会执行trackMotionScroll方法
/**
* int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
* Track a motion scroll
*/
boolean trackMotionScroll(int deltaY, int incrementalDeltaY)
final int childCount = getChildCount();
final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();
final Rect listPadding = mListPadding;
if (incrementalDeltaY < 0) //手指在Y方向上位置的改变量incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
else
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
if (cannotScrollDown || cannotScrollUp) //当不能滑动的时候则直接返回
return incrementalDeltaY != 0;
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)
// The view will be rebound to new data, clear any
// system-managed transient state.
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)
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0)
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
offsetChildrenTopAndBottom(incrementalDeltaY);
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
//
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY)
fillGap(down);
......
return false;
然后在第84行会进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据
/**
* @inheritDoc
*/
@Override
void fillGap(boolean down)
final int count = getChildCount();
if (down)
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK)
paddingTop = getListPaddingTop();
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
else
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK)
paddingBottom = getListPaddingBottom();
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected)
if (!mDataChanged)
// Try to use an existing view for this position.
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;
// 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;
不管怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。既然getActiveView()方法返回的值是null,那么还是会走obtainView()方法的
View obtainView(int position, boolean[] outMetadata)
outMetadata[0] = false;
final View scrapView = mRecycler.getScrapView(position);
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;
RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加
以上是关于ListView源码分析的主要内容,如果未能解决你的问题,请参考以下文章