RecycleBin机制
RecycleBin 为 AbsListView 中的一个内部类,因而所有继承自 AbsListView 的子类,即 ListView 和 GridView,都可以使用这个机制,这个机制保障了 ListView 实现上千条数据都不好OOM的最重要的一个原因
这里不放源码,只说明关键的参数及方法的作用。
关键参数
- int mFirstActivePosition ,第一个可见元素的 position 值
- View[] mActiveViews ,直接复用的view(处于可见状态的view)
- ArrayList[] mScrapViews,间接复用的view(处于不可见状态的view)。可以被适配器用作convert view 的无序 view 数组。特别指明:这里是一个数组,因为如果adapter中数据有多种类型,那么就会有多个ScrapViews。
- int mViewTypeCount,View 类型总数
- ArrayList mCurrentScrap,与 mScrapViews 类似,当 mViewTypeCount = 1情况下,mCurrentScrap = mScrapViews [0]
关键方法
-
void fillActiveViews(int childCount, int firstActivePosition),第一个参数为
mActiveViews
要存储的最少的view
的数量,第二个参数表示ListView
中第一个可见元素的 position 值,调用这个方法后,会根据传入的参数将ListView
中的指定元素存入mActiveView
数组中 -
View getActiveView(int position),与 fillActiveViews() 相对应,用于从
mActiveView
数组中 获取对应 View,该方法接受一个 position 参数,表示元素在ListView
中的位置,方法内部会将 position 的值自动转化为mActiveView
数组对应的下标值。特别之处在于,一旦找当指定位置相对应的 View,该 View 将被移除,下一次获取相同位置的 View 将会返回 null,即mActiveView
不能被复用 -
void addScrapView(View scrap, int position) ,scrap 为要添加的 View,position 为 View 在父类中的位置。放入时位置赋给 scrappedFromPosition 。有 transient 状态的 view 不会被 scrap(废弃),会被加入
mSkippedScrap
。就是将移出可视区域的 view,设置它的scrappedFromPosition,然后从窗口中 detach 该 view,并根据 viewType 加入到mScrapView
中。该方法会调用 mRecyclerListener 接口的函数 onMovedToScrapHeap
mRecyclerListener 的设置可通过 AbsListView 的 setRecyclerListener方法。
当 view 被回收准备再利用的时候设置要通知的监听器, 可以用来释放跟 view 有关的资源。
-
View getScrapView(int position),A view from the ScrapViews collection. These are unordered。内部实际上是调用 retrieveFromScrap() 方法。
ListView
ListView 继承自 View ,因而它一定会有三个执行步骤,onMeasure() 用于测量 View 的大小,onLayout() 用于确定 View 的布局,onDraw() 用于将 View 绘制到界面上;对于 ListView 来说 onDraw() 不是由其本身进行绘制,是由 ListView 当中的子布局进行绘制,因而不对此进行分析。
onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
//只计算了第一个 item
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
ListView 当中 onMeasure() 唯一需要注意的地方是,如果传进来的测量模式是 MeasureSpec.UNSPECIFIED 时,ListView 测量高度时只会计算第一个 item 的高度,例如 ListView 被ScrollView 嵌套时,否则将正常计算。
解决方式时重写 ListView 的 onMeasure() 方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
onLayout()
onLayout() 部分是 ListView 加载的关键。
通常情况下 View 都会进行至少两次 onMeasure() 和 onLayout() ,对其他 View 而言不必关心其到底执行了几次,因为执行的逻辑都是相同的。而在 ListView 当中则不一样,因为需要向其添加子元素,有可能会导致重复数据,因而需要分析两次 onLayout() 过程。
注:至少执行两次的原因是执行了两次 ViewRootImpl 的 performTraversals() 方法
第一次 onLayout()
ListView 当中是没有 onLayout() 方法的,onLayout() 在其父类 AbsListView 当中实现。
@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();
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
mInLayout = false;
}
onLayout() 方法中做了一个判断,如果 ListView 的大小或者位置发生了变化,那么 changed 变量就会变成 true ,此时会要求所有子布局都强制重绘,核心点在于 layoutChildren(),它的具体实现由 ListView 进行。
@Override
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;
//第一次 onLayout() 时值为 0
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_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);
}
//只有在数据源发生改变的情况下才为 true
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
...
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
// 第一次 onLayout() 时 ListView 内没有子 View ,暂时无效
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
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;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
...
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
在第一次 onLayout() 时,ListView 目前还没有任何的 View,且数据都还由 Adapter 管理,因此第24行 getChildCount() 方法得到的值为 0。接着会根据第93行 dataChanged 来决定执行逻辑,当数据源发生改变时,dataChanged 才会变成 true,否则都是 false,因此会进入第99行,调用 RecylceBin 中的 fillActiveViews() (将 ListView 中的子 View 进行缓存),此时 ListView 中没有任何的 View,暂时搁置。
接着会根据第106行 mLayoutMode 的值来决定布局模式,默认情况下都是 LAYOUT_NORMAL,即进入第146行 default 语句中,由于此时 childCount = 0,布局默认顺序从上往下,因此会进入第151行 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);
}
fillFromTop() 方法比较简单,就是从 mFirstPosition 开始,自顶至底去填充 ListView,接着就调用 fillDown() 方法
/**
* Fills the list from pos down to the end of the list view.
*
* @param pos The first position to put in the list
*
* @param nextTop The location where the top of the item associated with pos
* should be drawn
*
* @return The view that is currently selected, if it happens to be in the
* range that we draw.
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
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;
}
这里的 nextTop 是第一个子元素顶部距离整个 ListView 顶部的像素值,pos 是刚刚传入的 mFirstPosition,end 就是 ListView 底部减去顶部所得的像素值,mItemCount 是 Adapter 中元素数量。
因而一开始的情况下 nextTop 必定是小于 end 值的,并且 pos 也是小于 mItemCount 的,进入 while 循环,每次执行都会让 pos + 1,并且 nextTop 也会增加,当 nextTop >= end 时,也就是子元素超出当前屏幕了;或者 pos >= mItemCount 时,也就是 Adapter 中所有的元素都被遍历完,就会跳出 while 循环。
while 循环中关键的是第24行 makeAndAddView() 方法,代码如下:
/**
* 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.
*
* @param position logical position in the list
* @param y top or bottom edge of the view to add
* @param flow {@code true} to align top edge to y, {@code false} to align
* bottom edge to y
* @param childrenLeft left edge where children should be positioned
* @param selected {@code true} if the position is selected, {@code false}
* otherwise
* @return the view that was added
*/
@UnsupportedAppUsage
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;
}
这里的 mDataChanged 和 layoutChildren() 方法中的 dataChanged 是一样的逻辑,所以会尝试执行第20行,但由于此时 RecycleBin 中还没有缓存任何 View,所以得到的一定是 null。
继续尝试通过执行第31行的 obtainView() 方法来获得一个 View,而这次的 obtainView() 方法可以确保一定返回一个 View,再将获取到的 View 传入 setupChild() 方法中。obtainView() 方法位于 ListView 的父类 AbsListView 内,其代码如下:
/**
* Gets a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view
* is not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position the position to display
* @param outMetadata an array of at least 1 boolean where the first entry
* will be set {@code true} if the view is currently
* attached to the window, {@code false} otherwise (e.g.
* newly-inflated or remained scrap for multiple layout
* passes)
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
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) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
// If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
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();
}
}
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;
}
第一次尝试通过第23行 RecycleBin 的 getTransientStateView() 方法获取 transient 状态的 View,显然第一次 onLayout() 过程中为 null ,继续往下执行。
如果存在对应 position 的 transien t状态的 View,再判断 transientView 的 viewType 与这个 position 的 ViewType 是否一致。
如果 ViewType 一致,则调用 Adapter 的 getView() 方法获取 child,而 transientView 作为 convertView 参数。
如果得到的 child 的 View 与 transientView 不是同一个对象,比如 getView 中未使用 convertView,则将 child 添加进 ScrapView 缓存中。
第二次尝试通过第45行 RecycleBin 的 getScrapView() 方法从缓存中获取到 View,显然又为 null。
如果存在 ScrapView 最后同样判断得到的 child 的 View 与 ScrapView是不是同一个对象,不是则添加进 ScrapView 缓存。
第46行会将 ScrapView 当作参数传入 mAdapter 的 getView() 方法,而这个方法就是使用 ListView 过程中最经常重写的一个方法,此时传入的参数为 position, null 和 this。
写一个最为简单的 getView() 进行分析:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);
holder = new ViewHolder();
holder.textView = (TextView) convertView.findViewById(R.id.tv_list_content);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.textView.setText(mListData.get(position));
return convertView;
}
getView() 方法的三个参数为别是 position 代表当前子元素的位置;第二个参数 convertView ,刚刚传入的是 null,说明没有 convertView 可以复用,因此会调用 LayoutInflater 的 inflate() 方法来加载一共布局,并对这个 View 进行一些设定最后返回。
同时这个 View 也会作为 obtainView() 的结果返回,并最终传入到 setupChild() 方法当中。可以看出第一个 onLayout() 过程中,所有的子 View 都是通过调用 LayoutInflater 的 inflate() 方法加载出来的,这样就会相对比较耗时,但后续就不会再出现此类情况。
对子 View 的加载已经分析完了。
继续分析 setupChild() 方法
/**
* Adds a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child the view to add
* @param position the position of this child
* @param y the y position relative to which this view will be positioned
* @param flowDown {@code true} to align top edge to y, {@code false} to
* align bottom edge to y
* @param childrenLeft left edge where children should be positioned
* @param selected {@code true} if the position is selected, {@code false}
* otherwise
* @param isAttachedToWindow {@code true} if the view is already attached
* to the window, e.g. whether it was reused, or
* {@code false} otherwise
*/
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();
final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
|| child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make
// some up...
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
// Set up view state before attaching the view, since we may need to
// rely on the jumpDrawablesToCurrentState() call that occurs as part
// of view attachment.
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)) {
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);
//一些小语种国家的阅读习惯是从右向左,所以每次添加子View在布局中时都会调用这个方法
//可以在AndroidMenifest.xml 中 android:supportsRtl="false" 关闭
child.resolveRtlPropertiesIfNeeded();
}
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;
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
setupChild() 方法中的核心代码是通过调用第75行 addViewInLayout() 方法,将 obtainView() 方法获得到的子 View 添加到 ListView 当中。根据此前 fillDown() 方法中的 while 循环,会让子 View 逐渐将整个 ListView 控件填满最后跳出。
所以即使 Adapter 中有数千条数据,ListView 也只会加载第一屏的数据,其余的暂时不进行加载。
第二次 onLayout()
AbsListView 当中的 onLayout() 方法第一次和第一次执行是没有区别的,关键在于 ListView 当中实现的 layoutChildren() 方法,原因在于最后执行 setupChild() 方法时,会向 ListView 中添加数据,如果继续以相同的逻辑重复执行,ListView 中就会存在一份重复的数据。因此 ListView 在 layoutChildren() 方法里针对第二次 Layout 做了逻辑处理,解决了这个问题。
删除了大部分代码,只保留了关键逻辑
@Override
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;
//第一次 onLayout() 时值为 0
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
...
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
// 第一次 onLayout() 时 ListView 内没有子 View ,暂时无效
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
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;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
...
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
第二次 onLayout() 在第24行调用 getChildCount() 方法获取的子 View 数量是一屏可以显示的子 View 数量。
第46行 再次调用 RecycleBin 的 fillActiveViews() 方法,此次会将 ListView 当中所有的子 View 缓存到 RecycleBin 的 mActiveViews 数组中。
第50行会调用 detachAllViewsFromParent() 方法,这个方法会将 ListView 当中的所有子 View 全部清除掉,从而保证第二次 onLayout() 过程中不会产生一份重复的数据。由于此前已经将所有的子 View 都缓存到 RecycleBin 的 mActiveViews 数组中,重新加载 ListView 的子 View 只需要从中取出即可,不必再执行一边 inflate 过程,效率影响不大。
依旧根据第53行 mLayoutMode 的值来决定布局模式,继续从第93行的 default 开始执行,此次 childCount 不再等于 0,因此会进入 else 语句执行。else 语句中的执行判断,第一个是不成立的,因为默认情况下我们没有选中任何子元素,mSelectedPosition = -1。第二个逻辑判断通常是成立的,因为 mFirstPosition 的值一开始是等于 0 的,只要 adapter 中的数据大于 0 条件就成立。进入 fillSpecific() 方法中,代码如下:
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
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 {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
fillSpecific() 方法与 fillUp()、fillDown() 方法功能类似。主要区别在于第15行,fillSpecific() 会优先加载指定位置上的子 View,然后再加载该子 View 往上和往下的其他子 View。我们此次传入的 position 就是第一个子 View 的位置,所以 fillSpecific() 方法作用就基本与 fillDown() 方法相同。
继续分析 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;
}
第5行尝试从 RecycleBin 当中获取 ActiveView,此次已经可以获取到对应的 View了。那就不会再进入第16行的 obtainView() 方法中,将会直接进入 setupChild() 方法中。
setupChild() 方法中最后一个参数此次传入的是 true,这个参数代表当前的 View 是之前回收过的。
再次分析 setupChild() 方法:
删除了大部分代码,只保留了关键逻辑
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();
}
...
}
第一次和第二次 onLayout() 方法在 setupChild() 当中的区别就是,第一次执行第20行 addViewInLayout() 方法,第二次执行第6行 attachViewToParent() 方法。二者却别在于,如果我们要向 ViewGroup 中添加一个新的子 View,应该调用 addViewInLayout() 方法,而如果将此前 detach 的 View 重新 attach 到 ViewGroup 上,就应该调用 attachViewToParent() 方法。在第二次 layoutChildren() 方法中调用了 detachAllViewsFromParent() 方法,这样 ListView 中的所有子 View 都处于 detach 状态,所以这里调用 attachViewToParent() 方法。
所有的子 View 都经历了 detach 又 attach 的过程,ListView 的所有子 View 又可以正常的显示出来了。至此第二次 onLayout() 过程结束
滑动加载更多数据
onLayout 过程也只加载了第一屏的数据,剩余的数据的显示需要通过滑动部分来加载。
onTouchEvent() 方法位于 ListView() 的父类 AbsListView 当中,这里只分析滑动部分,也就是 onTouchMove() 方法内的代码。
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
if (mHasPerformedLongPress) {
// Consume all move events following a successful long press.
return;
}
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
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_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING://等了很久,用户的手势还是处于 down 状态
// Check if we have moved far enough that it looks more like a
// scroll than a tap. If so, we'll enter scrolling mode.
if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
break;
}
// Otherwise, check containment within list bounds. If we're
// outside bounds, cancel any active presses.
final View motionView = getChildAt(mMotionPosition - mFirstPosition);
final float x = ev.getX(pointerIndex);
if (!pointInView(x, y, mTouchSlop)) {
setPressed(false);
if (motionView != null) {
motionView.setPressed(false);
}
removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
mPendingCheckForTap : mPendingCheckForLongPress);
mTouchMode = TOUCH_MODE_DONE_WAITING;
updateSelectorState();
} else if (motionView != null) {
// Still within bounds, update the hotspot.
final float[] point = mTmpPoint;
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, motionView);
motionView.drawableHotspotChanged(point[0], point[1]);
}
break;
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL://越界滑动是指滑动区域超过了内容区域的首尾,表示ListView 没有更多的内容了
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}
第27行 startScrollIfNeeded() 方法会对手势作进一步的判断,如果是点击事件直接返回,如果是滑动事件一样会进入第54行的 scrollIfNeeded() 方法。
这里就不对ListView的滑动模式作进一步分析了,只针对核心加载逻辑分析。
想继续了解可以看:ListView 5种滑动模式解析全在这里了
继续分析 scrollIfNeeded() 方法:
删除了大部分代码,只保留了关键逻辑
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
...
if (y != mLastY) {
// We may be here after stopping a fling and continuing to scroll.
// If so, we haven't disallowed intercepting touch events yet.
// Make sure that we do so in case we're in a parent that can intercept.
if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
Math.abs(rawDeltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
final int motionIndex;
if (mMotionPosition >= 0) {
motionIndex = mMotionPosition - mFirstPosition;
} else {
// If we don't have a motion position that we can reliably track,
// pick something in the middle to make a best guess at things below.
motionIndex = getChildCount() / 2;
}
int motionViewPrevTop = 0;
View motionView = this.getChildAt(motionIndex);
if (motionView != null) {
motionViewPrevTop = motionView.getTop();
}
// No need to do all this work if we're not going to move anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
...
} else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
...
}
}
deltaY 表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY 则表示据上次触发event 事件手指在 Y 方向上位置的改变量。看第33行代码不难发现只要我们的手指在屏幕上有稍微一点点移动,就会调用第34行 trackMotionScroll() 方法,可以知道 ListView 的内容滚动核心逻辑就在这。
同时我们可以通过 incrementalDeltaY 的正负值情况来判断用户是向上还是向下滑动的了。如果 incrementalDeltaY 大于0,说明是向下滑动,否则就是向上滑动。
继续进入 trackMotionScroll() 方法:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
...
//省略的这部分代码是在作边界检测
final boolean down = incrementalDeltaY < 0;
...
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 {
...
}
}
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);
}
...
return false;
}
举例从数据下滑(对手势而言时上滑)部分进行分析,第22行是一个 for 循环目的是从上往下依次获取子 View,第24行在uo了一个判断,如果该子 View 的 bottom 小于 ListView 的 top 值,就说明这个子 View 已经不在屏幕内了,就会调用 RecycleBin 的 addScrapView() 方法将这个子 View 加入到废弃缓存中,并使用 count 统计有多少个子 View 被移出了屏幕。第37行 else 部分的内容为上滑与下滑部分类似不进行分析。
具体的将子 View 移出屏幕的操作在第46行,ListView 认为所有看不到的子 View 都应该移除,正是这个回收策略才保证了 ListView 的高性能。
第57行调用了 offsetChildrenTopAndBottom() 方法,参数是 incrementalDeltaY,目的是将所有的子 View 都按传入的参数进行的相应偏移,也就实现了随着手指的拖动,ListView 的内容跟随滚动的效果。
屏幕滚动的过程中,一部分子 item 向上(下) 滚动,ListView 将调用第65行的 fillGap() 方法来填充空白的部分。
对 fillGap() 方法进行分析:
fillGap() 方法由 ListView 实现
@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());
}
}
fillGap() 方法内部比较简单,就是对上滑还是下滑进行一个判断,再选择调用 fillDown() 还是 fillUp() 方法,这两个方法前面已经解释过作用了,就不再细说。fillDown() 内部加载子 View 的逻辑还是由 makeAndAddView() 方法实现,在 onLayout() 阶段已经使用了两次这个方法,这是第三次,走的逻辑还是不同,继续进行分析其原因。
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;
}
尝试从第5行 getActiveView() 方法来获取子 View,不过这次返回的结果为 null,原因在于 getActiveView() 方法内部实现的逻辑是一旦获取过指定位置的 View,该 View 将被移除,那么下一次返回的结果必定为 null,而第二次 onLyout() 的时候我们已经通过这个方法获取过一次数据了。结果为 null 则必须再次走第16行的 obtainView() 方法来加载子 View。
再回到 obtainView() 方法:
删除了大部分代码,只保留了关键逻辑
View obtainView(int position, boolean[] outMetadata) {
...
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;
}
第一次 onLayout() 的时候,我们就尝试过通过 RecycleBin 的 getScrapView() 方法来获取子 View,当时的结果为 null。而这次显然能够获取到对应的子 View,原因在于此前调用 trackMotionScroll() 方法时,移出屏幕的子 View 会立即缓存到 ScrapView 当中。
倘若对应 position 的子 View 在此前从未加载过,那么 getScrapView() 方法返回的结果为 null,此时返回的子 View 将是从 getView() 方法里重新 inflate() 得到的。
getScrapView() 方法内部会对 ItemViewType 作一个判断,如果为 1 ,则直接从 mCurrentScrap 数据中获取子 View,否则将从 mScrapViews[viewType] 数组中获取。
到这里所有的分析就结束了,可以发现 ListView 中的子 View 数量一共就几个,只是一直利用 RecycleBin 来交换数据,这样就能比较有效的避免出现 OOM 的情况。