ListView的一个很需要处理的,且很重要的点,就是如果处理数据的显示操作。一般在一个listView中会有很多数据,如果每个数据对应的view都预先缓存,那估计内存会爆了...所以ListView中采用的是对view进行复用的操作。因为每次展示的只有几个数据,也就是说只会用到几个view,所以ListView的做法就是将view进行复用,每当有新的数据进入屏幕也会伴随着旧的数据移出屏幕,所以只要拿这些“废弃”的view来复用,更新其中要展示的数据就可以了。对这些“废弃”了的view进行存储和复用的就是AbsListView中的RecycleBin类了。
RecycleBin
RecycleBin是ListView中用于对子view进行复用的,大致的流程就是,当我们的界面滚动时,会有一部分view移出屏幕,而同时会有另外的view进入到屏幕,所以我们可以通过对移出屏幕的view进行复用,这样就可以节省重新生成对象的时间,也节省了内存。
RecycleBin中的实例变量主要有这些:
//屏幕中第一个展示的数据的位置
private int mFirstActivePosition;
//在ListView开始进行layout的时候,会用这里面的view,在layout结束之后这些会移到mScrapViews中
//数组大小是屏幕最多能展示的view的数量,从mFirstActivePosition开始的data对应的view存到里面
private View[] mActiveViews = new View[0];
//相当于废弃的view,可以被Adapter进行复用
private ArrayList<View>[] mScrapViews;
//view的类型(ListView中的view可以有多种类型)
private int mViewTypeCount;
//当前使用的ScrapView的list,一般是mScrapView[0]
private ArrayList<View> mCurrentScrap;
//用于存储一些无法recycle的view(可能有特殊处理)
private ArrayList<View> mSkippedScrap;
/**
* 当废弃的view中的transient state不为空的时候,会将其放入以下两个map中,方便查找和直接复用view
* 貌似这两个现在还没用到?
*/
//相当于一个map,用position对应的index作为key,view作为value,在getTransientStateView()方法中会利用其寻找对应的view
private SparseArray<View> mTransientStateViews;
//基本同上,只是用position对应的id作为key
private LongSparseArray<View> mTransientStateViewsById;
RecycleBin中的一些方法:
fillAcitveViews
首先一个是fillAcitveViews,这个主要是用于对mActiveView数组进行初始化或者更新,代码如下:
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
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) {
activeViews[i] = child;
//记录当前对应的子view的位置
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
在ListView中实现了ViewGroup.LayoutParams的子类LayoutParams,在其中添加了子view的type,id,position等信息,这也更加方便我们对ListView中的子view进行记录。
getActiveView
当listView要进行layout的时候,会去用到mActiveView中的view,调用的方法是getActiveView:
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
//一旦mActiveView中的view被listView使用了,mActiveView就不再存储,防止内存泄露
activeViews[index] = null;
return match;
}
return null;
}
addScrapView
当listView中的数据发生改变,或者有某些view滑出了屏幕,就需要将这些view移到mScrapView中,具体实现的方法是addScrapView:
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
//没有Layout数据,不回收
if (lp == null) {
return;
}
//保存view所在的位置
lp.scrappedFromPosition = position;
final int viewType = lp.viewType;
//如果所在的viewType是不能回收的就放在mSkippedScrap
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
scrap.dispatchStartTemporaryDetach();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
final boolean scrapHasTransientState = scrap.hasTransientState();
//如果view具有transient State,不直接放入mScrapView中
if (scrapHasTransientState) {
//如果有itemId就存在mTransientStateViewsById中
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
//如果数据未改变,放入mTransientStateViews中
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
getSkippedScrap().add(scrap);
}
}
//放入mScrapView中
else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
getScrapView
当Adapter要复用到mScrapViews中的view的时候,就会调用getScrapView方法,代码如下:
View getScrapView(int position) {
//找到对应位置的view的type
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
//总共就一种type
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
}
//找到数组中对应的type的list中的view
else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
这里主要是调用了retrieveFromScrap方法来返回最终获得的view,该方法主要是从scrapView中找到一个view用于adapter的复用。看一下代码:
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// 判断我们是否有与该position或者id相同的view,有的话就直接复用
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =(AbsListView.LayoutParams) view.getLayoutParams();
//判断id是否相同
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
}
//判断position是否相同
else if (params.scrappedFromPosition == position) {
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
//如果没有就随意弹出一个返回
final View scrap = scrapViews.remove(size - 1);
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}
除了以上的方法之外,还有一些方法的功能就罗列在下面了
- markChildrenDirty():让所有List里面存储的view在下一次使用之前必须重新layout
- clear():清除掉所有的list中的数据
- getTransientStateView():获取mTransientStateViews和mTransientStateViewsById中的view(如果有)
- scrapActiveViews():将mActiveViews里面没有被layout用到的view放到mScrapViews中,如果有TransientState就放到mTransientStateViews或mTransientStateViewsById中
Layout
上面大概讲了RecycleBin中的一些方法,接下来看一下ListView中是怎么使用到RecycleBin的。ListView中最主要的使用是在onLayout中,代码在AbsListView中:
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();
//如果数据改变,则所有的view在使用前都需要重新layout
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);
}
}
可以看到主要的实现是在layoutChildren()方法来实现的,这也是在子类中根据需要来实现,ListView中也实现了相关代码,不过代码量有点大,就只拿跟RecycleBin相关的来看吧。
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
//后面要用到的一些参数
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
View oldFirst = null;
View newSel = null;
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
index = mNextSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
newSel = getChildAt(index);
}
break;
case LAYOUT_FORCE_TOP:
case LAYOUT_FORCE_BOTTOM:
case LAYOUT_SPECIFIC:
case LAYOUT_SYNC:
break;
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;
//如果ListView的数据改变需要进行的操作,主要是对一些参数进行更新
if (dataChanged) {
handleDataChanged();
}
//省略部分代码
...
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
//如果数据改变,原来的view不能直接使用,放到mScrapView中等待被复用
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
}
//将ListView需要的view放到mActiveView中
else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//将原来的view都detach
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
//根据不同的mode对ListView进行相应的view填充
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:
final int selectedPosition = reconcileSelectedPosition();
sel = fillSpecific(selectedPosition, mSpecificTop);
/**
* When ListView is resized, FocusSelector requests an async selection for the
* previously focused item to make sure it is still visible. If the item is not
* selectable, it won't regain focus so instead we call FocusSelector
* to directly request focus on the view after it is visible.
*/
if (sel == null && mFocusSelector != null) {
final Runnable focusRunnable = mFocusSelector
.setupFocusIfValid(selectedPosition);
if (focusRunnable != null) {
post(focusRunnable);
}
}
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;
}
// 将mActiveView中没有被使用的view放回到mScrapView中
recycleBin.scrapActiveViews();
// remove any header/footer that has been temp detached and not re-attached
removeUnusedFixedViews(mHeaderViewInfos);
removeUnusedFixedViews(mFooterViewInfos);
//省略部分代码
...
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
在layoutChildren中,先是判断数据是否改变,如果数据改变,则所有的view在重新被使用之前,都需要重新layout;反之,原来ListView中的view可以直接复用,将其放在mActiveView中等待使用。接下来对ListView进行填充操作,最后将mActiveView中没有用到的view放到mScrapView中。代码中根据不同的mode会选择相应的fillXXX方法,看了一下这些代码,最终往ListView中添加view调用的都是makeAndAddView方法:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 直接使用一个已有的view
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// 生成一个新的view,或者找到一个可以复用的view
final View child = obtainView(position, mIsScrap);
// 需要重新measure
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
在该方法中,先看一下能不能直接使用原来的view,如果不行的话就复用mScrapViews中的或者重新生成一个view,后面一步的操作是在obtainView中,代码是在AbsListView中实现:
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
//view之前是否已经用过,主要用于后续是否需要重新measure之类的
outMetadata[0] = false;
//判断是否能根据transient state来获取view
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
//类型不变
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
//rebind数据失败,将updatedView放到mScrapView中(因为不会用到)
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
outMetadata[0] = true;
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
//从mScrapView中选择一个view,如果选择不到就返回null
final View scrapView = mRecycler.getScrapView(position);
//判断能够rebind数据
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
//rebind数据失败,将其重新放回mScrapView中
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
//对view的各种设置
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
setItemViewLayoutParams(child, position);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}
在代码中判断RecycleBin中的存储的view能否被复用,或者需要重新生成view,同时调用了Adapter中的getView方法来判断能够重新绑定数据。最终会返回一个reuse的或者是新的view。一般Adapter需要我们自己重写,getView方法也是我们自己实现的,一般在实现的过程中需要判断view是否为null,如果不为null,我们应该对其进行复用。这样可以节省空间。
在makeAndAddView方法中,一旦我们获取到一个view之后,接下来要进行的操作就是setChild,看一下该代码:
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
final boolean isSelected = selected && shouldShowSelector();
final boolean updateChildSelected = isSelected != child.isSelected();
final int mode = mTouchMode;
final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
&& mMotionPosition == position;
final boolean updateChildPressed = isPressed != child.isPressed();
//判断是否要measure,用的就是obtainView中的outMetadata[0]
//如果view是重新inflate的,就需要重新measure
//或者view之前的selected状态和当前需要的selected状态不一样也需要重新measured
final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
|| child.isLayoutRequested();
//尽量复用原来的layoutParams
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
//各种状态设置
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
if (child instanceof Checkable) {
((Checkable) child).setChecked(mCheckStates.get(position));
} else if (getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
child.setActivated(mCheckStates.get(position));
}
}
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
// 将view重新attached
attachViewToParent(child, flowDown ? -1 : 0, p);
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;
}
// 在layout过程中将这个view添加进来
addViewInLayout(child, flowDown ? -1 : 0, p, true);
// add view in layout will reset the RTL properties. We have to re-resolve them
child.resolveRtlPropertiesIfNeeded();
}
//对child view进行measure操作
if (needToMeasure) {
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
//如果是新inflate的view,需要进行layout操作,确定位置
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
}
//如果是reuse的view,可以重新设置一下其对应的布局位置就可以
else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true)
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
到这里,我们整个listView的layout过程就基本OK了,其中对RecycleBin的使用也大致讲了一下,总体的流程大致是这样的:
- onLayout:判断数据是否改变,如果改变,则listView中的view和RecycleBin中的view在使用之前都要重新layout,调用layoutChildren方法
- layoutChildren:先将原来listView中的数据放入mActiveView或者mScrapView中,并将其detach,根据我们定义的layout的mode对ListView进行填充操作,调用相应的fillXXX方法。
- fillXXX:用循环来判断是否需要往ListView里面添加view(是否可见),如果可以添加,就调用makeAndAddView方法进行添加。
- makeAndAddView:从RecycleBin的mActiveViews中或mScrapViews获取view,如果获取不到就inflate一个view,对得到的view调用setupChild方法添加到ListView中。
- setupChild:如果view是复用的,则重新attach并调整相应的layout参数,如果是新生成的,则将其添加到ListView中,并进行measure,layout等操作。
有些地方还没有看的比较深入,先大概这么写一下流程吧,以后有新的理解的话再来更新。