在之前的博客已经介绍了ListView和Adapter了,但是之前篇文章只是着眼于基本的使用并没有讲到具体的背后的原理,这篇博客将会代大家过下ListView的源代码,让大家了解下整个原理:重点是ListView 缓存机制

ListView 缓存机制的实现
RecycleBin原理概述

在介绍RecycleBin原理之前我们先来介绍两个对象,ActiveView和ScrapView。我们知道ListView中包含两类子View,一类是可见的,显示在屏幕上的,这个就是ActiveView,另一类是不可见的这些被称为ScrapView(Scrap表示废弃的意思)ListView会把ScrapView删除的同时放入到RecycleBin中缓存起来。当我们滑动ListView的时候,就会导致有一部分元素进入屏幕,一些item从屏幕中移出,在移入的时候会从RecycleBin中取出一个ScrapView,将其作为convertView参数传递给Adapter的getView方法,从而达到View复用的目的,这样就不必在Adapter的getView方法中执行 LayoutInflater.inflate()方法了,从而大大提高整个性能,在编程的世界中有两个东西一直是矛盾的那就是时间和空间,而ListView中在这两者之间做了很好的平衡,个人觉得之所以可以做到这点还依赖于ListView item的一个特性,那就是每个item的布局是一致的只不过换了内容。

class RecycleBin {

//.....
/**
* Views that were on screen at the start of layout. This array is populated at the start of
* layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
* Views in mActiveViews represent a contiguous range of Views, with position of the first
* view store in mFirstActivePosition.
*/
private View[] mActiveViews = new View[0];

/**
* Unsorted views that can be used by the adapter as a convert view.
*/
private ArrayList<View>[] mScrapViews;
//.....
/**
* Fill ActiveViews with all of the children of the AbsListView.
*
* @param childCount The minimum number of views mActiveViews should hold
* @param firstActivePosition The position of the first view that will be stored in
* mActiveViews
*/
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;

//noinspection MismatchedReadAndWriteOfArray
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
// Remember the position so that setupChild() doesn't reset state.
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}

/**
* Get the view corresponding to the specified position. The view will be removed from
* mActiveViews if it is found.
*
* @param position The position to look up in mActiveViews
* @return The view if it is found, null otherwise
*/
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;
}

/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}

/**
* Puts a view into the list of scrap views.
* <p>
* If the list data hasn't changed or the adapter has stable IDs, views
* with transient state will be preserved for later retrieval.
*
* @param scrap The view to add
* @param position The view's position within its parent
*/
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
// Can't recycle, but we don't know anything about the view.
// Ignore it completely.
return;
}

lp.scrappedFromPosition = position;

// Remove but don't scrap header or footer views, or views that
// should otherwise not be recycled.
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
// Can't recycle. If it's not a header or footer, which have
// special handling and should be ignored, then skip the scrap
// heap and we'll fully detach the view later.
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}

scrap.dispatchStartTemporaryDetach();

// The the accessibility state of the view may change while temporary
// detached and we do not allow detached views to fire accessibility
// events. So we are announcing that the subtree changed giving a chance
// to clients holding on to a view in this subtree to refresh it.
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

// Don't scrap views that have transient state.
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
// If the adapter has stable IDs, we can reuse the view for
// the same data.
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
// If the data hasn't changed, we can reuse the views at
// their old positions.
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
// Otherwise, we'll have to remove the view and start over.
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}

if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
}

RecycleBin的代码不是很大但是全部在博客讲解每个细节一来自己也不是完全懂每个细节,而且太关注细节我们常常会陷入细节而看不到整个原理,所以在分析源代码的时候,第一次不要太纠结细节,等到后面有疑问或者遇到问题需要解决的时候在认真分析对应的细节。
好了言归正传,我们上面贴出了RecycleBin的关键代码,正如上面介绍的RecycleBin包含mActiveViews,以及mScrapViews这两个主要成员变量,这里再插入一个分析源代码的方法就是:在学习源代码的时候先看英文注释,一般Android代码中大部分都有比较详细的注释的,通过这些注释往往会快速了解这东西到底是干吗的,到底要不要继续深入看下去。

我们看下mActiveViews 的注释:

Views that were on screen at the start of layout. This array is populated at the start of layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.Views in mActiveViews represent a contiguous range of Views, with position of the first view store in mFirstActivePosition.

大体的意思就是它用于存放的是在每次开始布局之前位于屏幕上的那些item View,在布局结束后所有存在于mActiveViews的item View都会移到mScrapViews,mActiveViews存放的内容是连续的从mFirstActivePosition位置开始到屏幕所能显示下的最大item数目。

那么mScrapViews 又是什么呢?

Unsorted views that can be used by the adapter as a convert view.

从注释中可以了解到它是一个无序排列的列表,这些视图可以被传入Adapter中作为一个convert view.

  • fillActiveViews 这个方法用于将AbsListView所有的子item 添加到ActiveViews中,它接收两个参数childCount表示屏幕显示的子item的数目,firstActivePosition表示屏幕上第一个item的位置。这个很简单。
  • getActiveView 这个方法将会传入位置参数,然后将会从mActiveViews中寻找并取出item View,如果找到在mActiveViews中对应的这个位置View将会被置为null
  • addScrapView 这个代码比较多,但是所处理的任务很简单就是将废弃的View添加到mScrapViews中,getScrapView相反就是从ScrapView中取出view。

大家看完上面的代码是不是和我有一样的感觉,这代码有啥营养?确实如果不结合ListView代码来分析,是完全看不出RecycleBin的原理的。我们在了解到RecycleBin中有什么之后皆来来就需要看它怎么在Listview中起到一个缓存的作用。

我们接下来就来讲解下ListView和RecycleBin如何交互实现缓存的:

主要关注如下两个情况:

  • ListView的item View 回收到 RecycleBin
  • 从RecycleBin中取出View作为ListView的item View

我们从绘制的角度出发,观察它是如何在绘制的过程中实现子View的缓存:

首先在分析代码之前我们先来了解下ListView的继承关系,这样有利于我们对代码的了解(说白了就是知道在子类找不到的时候,知道如何顺着继承关系找)

ListView-> AbsListView ->AdapterView -> ViewGroup
@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;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}

onlayout方法是在AbsListView中实现的,ListView中并没有实现这个方法,这是因为这部分代码属于GridView以及ListView通用的,所以将其放在父类上,我们知道在布局大小或者位置等发生变化的时候将会调用onLayout()方法,它会强制要求所有子item进行重绘。但是如果仔细看会发现layoutChildren()这个方法,我们接下来看下这个方法:

@Override
protected void layoutChildren() {
//......
try {
//......
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
// Remember stuff we will need down below
switch (mLayoutMode) {
//......
case LAYOUT_MOVE_SELECTION:
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}

//......

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);
}

// Clear out old views
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;
}

// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
//......
} finally {
//......
}
}

在介绍之前我们先看下如下代码片:

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);
}

我们需要了解下dataChanged这个变量是怎么控制的,其实这个变量是在Adapter调用了notifyDataSetChanged方法,通知Adapter的数据源发生了变化,此时dataChanged变量就为true,这时候会将当前的所有可视item 通过RecycleBin的addScrapView方法将其放入RecycleBin的废弃List中,供后续复用。

为了让大家地更清楚我把那些无用的代码删除掉了,我们先从第一次layout的情形进行分析,第一次layout的时候布局上是没有子元素的,这时候dataChanged = false,childCount = 0所以fillActiveViews这个方法也是没有任何作用的。所以上面的整个关键代码如下:

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);
}
}

上面代码就是根据实际的方向来调用fillFromTop还是fillUp这两个其实功能都差不多,区别只是在于方向而已,我们以fillFromTop来分析:

/**
* Fills the list from top to bottom, starting with mFirstPosition
*
* @param nextTop The location where the top of the first item should be
* drawn
*
* @return The view that is currently selected
*/
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);
}

片段没实在的作用,实际的功能位于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) {
// 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;
}

在fillDown中会从屏幕的第一个元素开始,遍历填充每个元素,这里最关键的代码是makeAndAddView

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

在makeAndAddView方法中尝试从从RecycleBin当中获取一个ActiveView,但是在第一次布局时候RecycleBin是空的,所以返回的是null,那么就直接调用obtainView方法创建或者尝试从ScropView中或其一个子View来复用,但是这时候ScropView数组也是空的,所以只能新建一个,这个就是在obtainView方法中实现的逻辑了,我们现在这里提下,紧接着就将获取到的子View通过setupChild添加到ListView中。

因此我们重点关注下obtainView:

View obtainView(int position, boolean[] isScrap) {
//.......
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 {
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
//.......
return child;
}

obtainView方法也是又臭又长但是我们只看关键的。它会调用getScrapView从mRecycler的ScrapView中获取可以复用的scrapView但是我们现在第一次什么都没有,所以scrapView为null。下面这个大家熟悉了把,

final View child = mAdapter.getView(position, scrapView, this);

什么不熟悉?这个就是调用Adapter的getView方法啊,第二个参数就是我们经常提到的convertView。想必看了下面的代码大家都会有印象吧:

public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder=new ViewHolder();
convertView = mInflater.inflate(R.layout.vlist2, null);
holder.img = (ImageView)convertView.findViewById(R.id.img);
holder.title = (TextView)convertView.findViewById(R.id.title);
holder.info = (TextView)convertView.findViewById(R.id.info);
holder.viewBtn =(Button)convertView.findViewById(R.id.view_btn);
convertView.setTag(holder);
}else {
holder = (ViewHolder)convertView.getTag();
}
holder.img.setBackgroundResource((Integer)mData.get(position).get("img"));
holder.title.setText((String)mData.get(position).get("title"));
holder.info.setText((String)mData.get(position).get("info"));
return convertView;
}

第一次layout的时候我们convertView为空那么就会inflate一个作为convertView并返回。makeAndAddView#setupChild就将这个convertView添加到布局中。

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
//......
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
//......
}

setupChild方法很简单就是调用了addViewInLayout将convertView添加到ListView中。

接下来我们看下在经过第一次layout后,往后的布局和之前的布局有什么不一样的地方。
我们还是从layoutChildren开始,这里和上面的区别是由于childCount不为0所以fillActiveViews会将Child Item添加到ActiviteView数组中,
紧接着调用fillSpecific,这个会从指定的位置开始加载Child item。紧接着调用makeAndAddView,这时候makeAndAddView跑的逻辑就和之前不一样了,由于mDataChanged为false(假设当前数据集每变,也就是没有对数据集进行增删的操作)这次调用mRecycler.getActiveView的时候返回的就不是空了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存ChildView。所以就不会再进入obtainView()方法,而是会直接调用setupChild()方法,这样就避免了重新inflate。
接下来看下setupChild(),由于我们在这之前调用了detachAllViewsFromParent所以子View应该调用attachViewToParent()方法。而不是前面提到的addViewInLayout方法。

好了我们看完上面代码可能觉得还是没有接触到最核心的缓存机制,最关键的部分是在滑动的时候如何缓存的,接下来我们看下这部分逻辑:
我们知道我们的交互事件都会被传递到onTouchEvent中,在ListView也不例外,onTouchEvent有很多事件,我们关注ACTION_MOVE,在这部分代码中我们重点关注trackMotionScroll方法:这个方法顾名思义就是跟踪滑动事件:这个代码也很长,我们还是从中关注重点。

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}

final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();

final Rect listPadding = mListPadding;

// "effective padding" In this case is the amount of padding that affects
// how much space should not be filled by items. If we don't clip to padding
// there is no effective padding.
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = listPadding.top;
effectivePaddingBottom = listPadding.bottom;
}

// FIXME account for grid vertical spacing too?
final int spaceAbove = effectivePaddingTop - firstTop;
final int end = getHeight() - effectivePaddingBottom;
final int spaceBelow = lastBottom - end;

final int height = getHeight() - mPaddingBottom - mPaddingTop;
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}

if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}

final int firstPosition = mFirstPosition;

// Update our guesses for where the first and last views are
if (firstPosition == 0) {
mFirstPositionDistanceGuess = firstTop - listPadding.top;
} else {
mFirstPositionDistanceGuess += incrementalDeltaY;
}
if (firstPosition + childCount == mItemCount) {
mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
} else {
mLastPositionDistanceGuess += incrementalDeltaY;
}

final boolean cannotScrollDown = (firstPosition == 0 &&
firstTop >= listPadding.top && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

if (cannotScrollDown || cannotScrollUp) {
return incrementalDeltaY != 0;
}

final boolean down = incrementalDeltaY < 0;

final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}

final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();

int start = 0;
int count = 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();
}

// invalidate before moving the children to avoid unnecessary invalidate
// calls to bubble up from the children all the way to the top
if (!awakenScrollBars()) {
invalidate();
}

offsetChildrenTopAndBottom(incrementalDeltaY);

if (down) {
mFirstPosition += count;
}

final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}

if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(mSelectedPosition, getChildAt(childIndex));
}
} else if (mSelectorPosition != INVALID_POSITION) {
final int childIndex = mSelectorPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(INVALID_POSITION, getChildAt(childIndex));
}
} else {
mSelectorRect.setEmpty();
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
return false;
}

这个方法传入两个参数deltaY表示从手指最初按下时的位置到当前手指所处的位置,incrementalDeltaY则表示相邻两次在Y方向上位置的改变量,incrementalDeltaY的正负值就可以判断我们当前的滑动方向了。(incrementalDeltaY小于0,表示向下滑动,大于0就是向上滑动)在这个部分逻辑中将会根据边缘作为判断依据如果子View的bottom值小于top值的时候,说明这个子View移出屏幕了,这时候就会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,上面介绍了移出屏幕的情况,由于界面显示的子View是固定的所以有移出就有移入,我们接下来看下这部分逻辑:
如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调 用fillGap()方法,我们看下这个方法:

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());
}
}

额,又回到之前的流程,在fillGap又调用了fillDown/fillUp,我们知道这两个方法会调用makeAndAddView,但是这时候的makeAndAddView流程又和上面不大一样了。这时候mRecycler.getActiveView返回的是null

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

为什么是null我们再来看下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;
return match;
}
return null;
}

由于之前这个方法已经被调用过了它会将activeViews[index]置为null所以还会调用obtainView,上面已经对该方法做了分析,它会调用getScrapView()方法来尝试从废弃缓存中获取一个View,如果没有的话则会inflate一个返回。

ListView的优化

上面介绍的只是ListView的缓存机制,了解了整个缓存机制后我们就可以充分利用convertView来判断是否inflate了,所以整个ListView只加载一屏的布局,之后滑动出来的item使用的是之前已经加载的布局的缓存,但是我们看下下面的getView我们使用了ViewHolder,还有set/getTag这是干啥用的?

public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder=new ViewHolder();
convertView = mInflater.inflate(R.layout.vlist2, null);
holder.img = (ImageView)convertView.findViewById(R.id.img);
holder.title = (TextView)convertView.findViewById(R.id.title);
holder.info = (TextView)convertView.findViewById(R.id.info);
holder.viewBtn =(Button)convertView.findViewById(R.id.view_btn);
convertView.setTag(holder);
}else {
holder = (ViewHolder)convertView.getTag();
}
holder.img.setBackgroundResource((Integer)mData.get(position).get("img"));
holder.title.setText((String)mData.get(position).get("title"));
holder.info.setText((String)mData.get(position).get("info"));
return convertView;
}

其实使用ViewHolder,还有set/getTag是为了节省findViewById的时间。如果不使用ViewHolder,每次getView的时候都需要得到一次子布局,而这也是很耗时并且耗资源的,如果使用了ViewHolder作为子布局的缓存,使用View的setTag方法将缓存与每个item绑定,则也可以省去了findViewById的时间(这里我个人的理解是将id与ViewHolder中的View绑定起来,如果有理解错误欢迎更正)
这里还需要注意一点我们一般需要将ViewHolder设置为静态的因为因为静态内部类,不持有外部类的引用,从而避免内存泄露。

总结如下:基本的优化方式

  • 使用ConvertView复用机制
  • 使用ViewHolder
  • 使用set/getTag

ListView Item带有图片的情况:

我们在开发的时候会遇到ListView中每个item都有一个Image的情况,这个在Music应用中十分常见,
所以我们一般对这些图像的加载使用图片缓存,并且在加载这些图片的时候使用异步加载,但是这也面临这空间和时间的平衡问题。
这种情况还有一种方法就是针对图片资源进行优化比如在图片解码的时候,降低像素颜色信息,去掉透明度等,或者在设计资源的时候尽量减小图片资源的尺寸。

为了避免不需要加载在我们还可以设置在滑动不加载图片滑动停止的时候加载图片
下面是对应的实现例子:
转载自 http://blog.csdn.net/yy1300326388/article/details/45153813

//定义当前listview是否在滑动状态
private boolean scrollState=false;
public void setScrollState(boolean scrollState) {
this.scrollState = scrollState;
}
//实体类
UserEnity userEnity=lists.get(position);
if (!scrollState){//如果当前不是滑动的状态,我们填充真数据
//填充数据
viewHolder.tv_name.setText(userEnity.getName());
//设置Tag中数据为空表示数据已填充
viewHolder.tv_name.setTag(null);
//加载图片
ImageLoader.getInstance().displayImage(img_url,viewHolder.iv_icon);
//设置tag为1表示已加载过数据
viewHolder.iv_icon.setTag("1");

}else{//如果当前是滑动的状态,我们填充假数据
viewHolder.tv_name.setText("加载中");
//将数据name保存在Tag当中
viewHolder.tv_name.setTag(userEnity.getName());
//将数据image_url保存在Tag当中
viewHolder.iv_icon.setTag(img_url);
//设置默认显示图片(最好是本地资源的图片)
viewHolder.iv_icon.setImageResource(R.mipmap.ic_launcher);

}
设置监听
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE://停止滚动
{
//设置为停止滚动
myAdapter.setScrollState(false);
//当前屏幕中listview的子项的个数
int count = view.getChildCount();
Log.e("MainActivity",count+"");

for (int i = 0; i < count; i++) {
//获取到item的name
TextView tv_name = (TextView) view.getChildAt(i).findViewById(R.id.main_item_tv_name);
//获取到item的头像
ImageView iv_show= (ImageView) view.getChildAt(i).findViewById(R.id.main_item_iv_icon);

if (tv_name.getTag() != null) { //非null说明需要加载数据
tv_name.setText(tv_name.getTag().toString());//直接从Tag中取出我们存储的数据name并且赋值
tv_name.setTag(null);//设置为已加载过数据
}

if (!iv_show.getTag().equals("1")){//!="1"说明需要加载数据
String image_url=iv_show.getTag().toString();//直接从Tag中取出我们存储的数据image——url
ImageLoader.getInstance().displayImage(image_url, iv_show);//显示图片
iv_show.setTag("1");//设置为已加载过数据
}
}
break;
}
case AbsListView.OnScrollListener.SCROLL_STATE_FLING://滚动做出了抛的动作
{
//设置为正在滚动
myAdapter.setScrollState(true);
break;
}
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL://正在滚动
{
//设置为正在滚动
myAdapter.setScrollState(true);
break;
}
}
}

局部更新:
http://blog.csdn.net/nupt123456789/article/details/39432781
http://www.cnblogs.com/liuling/p/2015-10-20-01.html

private void updateProgressPartly(int progress,int position){
int firstVisiblePosition = listview.getFirstVisiblePosition();
int lastVisiblePosition = listview.getLastVisiblePosition();
if(position>=firstVisiblePosition && position<=lastVisiblePosition){
View view = listview.getChildAt(position - firstVisiblePosition);
if(view.getTag() instanceof ViewHolder){
ViewHolder vh = (ViewHolder)view.getTag();
vh.pb.setProgress(progress);
}
}
}

总结一下:

其实ListView优化的最根本途径在于getView方法的优化,所以我们优化就需要将一切耗时的操作从getView中抽离,比如图片加载,网络数据加载,文件加载。
因此可以通过如下几种途径来优化:

  1. 启动线程来异步加载图片,在图片尚未加载完成之前先用空白图片占位
  2. 滑动时不加载图片,停止滑动时加载
  3. 使用缓存机制将其缓存到内存中(注意使用弱引用)
  4. 局部刷新
  5. Item中的控件宽高尽量写成固定的值或者math_parent,避免影响其他控件的位置导致重新绘制
  6. 减少布局层次,缩短绘制时间。慎用 layout_weight 类似属性,以便缩短布局的 Measure 时间。
  7. 使用RecyclerView,RecyclerView提供了原生的局部刷新功能
  8. 在加载的时候,为ImageView设置一个Tag,比如imageView.setTag(image_url),下一次再加载之前,首先获取Tag,比如imageUrl = imageView.getTag(),如果此时的地址和之前的地址一样,我们就不需要加载了,如果不一样,再加载。

最后是之前开发中遇到的两种ListView错位的问题:
下面是当时的参考解决方案,后续有空的时候再对这部分进行专门总结:
http://www.cnblogs.com/lesliefang/p/3619223.html
http://www.runoob.com/w3cnote/android-tutorial-listview-checkbox.html
http://www.trinea.cn/android/android-listview-display-error-image-when-scroll/

Contents
  1. 1. ListView 缓存机制的实现
    1. 1.1. RecycleBin原理概述
  2. 2. ListView的优化