现在网上优化listview的内容一大把,重用convertView,viewholder,但是分析listview的却很少,很多人都不清楚为什么这么做能优化listview
或者不清楚listview的重用机制,实现机制。今天特地研究了下源码,开个帖子,记录下。
一:
listview依次继承AbsListView,AdapterView,ViewGroup,View
首先,view第一步都是onMeasure计算高宽,我们来分析下是如何计算宽高的。我们找到listview的onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
.....
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
measureScrapChild(child, 0, widthMeasureSpec);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
.....
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;
}
View child = obtainView(0, mIsScrap) 创建了position为0(也就是列表第一行)的列表项,measureScrapChild(child, 0, widthMeasureSpec) 调用列表项的measure计算宽高mRecycler.addScrapView(child, 0); 放入RecycleBin中之后复用。heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); 计算listview总高。--------------------上面是总体逻辑,现在我们进到方法里面看看具体是如何实现的-----------------obtainView(0, mIsScrap) 这个方法非常重要,是listview实现convertView重用的关键方法。我们贴下源码:</pre><pre name="code" class="java">View obtainView(int position, boolean[] isScrap) {
....... // 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);
}
}
// Scrap view implies temporary detachment.
isScrap[0] = true;
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 {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
<span style="white-space:pre"> </span> ....
return child;
}
此方法里分两个逻辑处理 a:View transientView = mRecycler.getTransientStateView(position); 从transient状态中获取是否有缓存的view,</span>View updatedView = mAdapter.getView(position, transientView, this); 这个方法是不是很惊喜,大家终于知道我们的adapter中的getview方法是从
哪调用的了吧,如果在transient状态中获取到了缓存的view,就会传入到convertView中。View scrapView = mRecycler.getScrapView(position); 从scrap状态中获取缓存view,View child = mAdapter.getView(position, scrapView, this);同上,如果获取到了传入adapter的getview中。OK 讲到这里要说下RecycleBin,RecycleBin就是listview实现列表项重用的实现类了,RecycleBin将列表项分为active跟scrap两类,active里存放当前可见的列表项,scrap类里放着移出屏幕的废弃列表项。我们继续跟踪mRecycler.getScrapView(position) 看看里面是如何实现。<span style="font-family: Arial, Helvetica, sans-serif;">View getScrapView(int position) {
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
}
return null;
}
</span><span style="font-family: Arial, Helvetica, sans-serif;">判断了列表项的类型是一种还是多种,我们先只看一种的情况下,明白了一种的情况,多种TYPE情况下也是一样的。我们跟踪进入retrieveFromScrap方法。</span><span style="font-family: Arial, Helvetica, sans-serif;">private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position or ID.
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =
(AbsListView.LayoutParams) view.getLayoutParams();
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} 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;
}
}
</span>先遍历了scrap列表,如果有id匹配或者position匹配就返回列表项,如果全都不匹配,View scrap = scrapViews.remove(size - 1);获取scrap列表最后一项返回。OK,跟到这里 obtainView()方法的逻辑就结束了。接下来我们看listview总高是如何产生的,方法measureHeightOfChildren()</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
final int maxHeight, int disallowPartialChildPosition) {
....
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
// Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
}
returnedHeight += child.getMeasuredHeight();
if (returnedHeight >= maxHeight) {
// We went over, figure out which height to return. If returnedHeight > maxHeight,
// then the i'th position did not fit completely.
return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight;
}
if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}
return returnedHeight;
}
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">重点是这个for循环,循环调用obtainView(i, isScrap) 依次创造列表项,然后measure列表项的宽高,returnedHeight += child.getMeasuredHeight() 累计列表总高,if (returnedHeight >= maxHeight) 当高超出maxHeight return 返回高度。在上面分析了单个列表项的源码分析后,这里理解起来就很简单了。二:到现在高宽已经计算好了,就到了onlayout这一步了,我们在listview的父类AbsListView中找到了onlayout方法。</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
...
layoutChildren(); ...
}
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">没发现有什么重要内容,但是看到layoutChildren()这个,回到listview中找到这个方法,原来重要内容都在这里。</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">protected void layoutChildren() { .....
// 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 {
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:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
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();
.....<span style="white-space:pre"> </span> ..... }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">源码中layoutChildren()内容非常多。。好重量级的方法。。。我们又要找重点了。
重点一:if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
将当前的所有列表项先放入recycleBin缓存,以备后续重用。重点二:判断了mLayoutMode,分别调用fillFromSelection,fillFromMiddle,fillSpecific,fillUp,fillFromTop好多fill方法,但其实这些方法的内部实现机制也是差不多的,我们挑一个来看,fillFromTop();</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">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);
}
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;"><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">fillFromTop()调用了 fillDown();</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">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;
}
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">这里是一个while循环,nextTop < end 表示列表总高还能容下列表项,pos < mItemCount 当前position比总数小就不停得产生列表项,我们看这个产生列表项的方法进去看看child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">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;
}
这里又出现两个分支,if (!mDataChanged) 如果数据未改变,
child = mRecycler.getActiveView(position); 从recycleBin中的active类中获取缓存view,否则 child = obtainView(position, mIsScrap); 又是这个obtainview方法,这个方法上面已经讲过获取缓存view的逻辑了。所以看到这里,基本能明白listview在创建过程中重用view的机制了。我们接着看,不论上面的哪一个分支,都会走到setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);这个方法中。</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) { ....<span style="white-space:pre"> </span>....
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);
}
<span style="white-space:pre"> </span>....
<span style="white-space:pre"> </span>.... }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">重点是attachViewToParent(child, flowDown ? -1 : 0, p);跟addViewInLayout(child, flowDown ? -1 : 0, p, true);这两个方法。这两个方法走进去最后都会到Viewgroup的addInArray(child, index);这个方法是将view添加到Viewgroup中去的方法。这下明白了,原来listview是在onlayout过程中将一条条列表项添加到viewgroup中,listview的本质就是一个viewgroup,里面一大堆整齐排列的列表项。最后,listview代码量非常庞大,好几万行,看起来确实有点累 0.0 本人水平有限,文中如有错误,欢迎指正。
本文深入解析了Android中ListView的工作原理及其实现机制,包括ListView的测量过程、布局过程以及convertView和viewholder的重用机制。

9354

被折叠的 条评论
为什么被折叠?



