Android进阶之路系列:http://blog.csdn.net/column/details/16488.html
在Android开发中我们经常使用ListView和GridView,它们都有一套缓存机制,通过复用防止view的不停创建。
ListView和GridView都是AbsListView的子类,使用RecycleBin来进行view的缓存。
1、View的Transient状态
要想搞懂RecycleBin的缓存机制,我们首先要了解Transient和Scrap都是什么。
Transient是View的一种状态,可以通过View的hasTransientState函数来判断,官方解释如下:
A view with transient state cannot be trivially rebound from an external data source, such as an adapter binding item views in a list. This may be because the view is performing an animation, tracking user selection of content, or similar.
从解释上看,Transient是指View的一种不稳定状态,是瞬时状态,比如说正在执行一个动画,有可能下一秒就改变了。
而Scrap则是ListView和GridView的缓存状态,当一个Item不可见被回收后存入缓存。
2、RecycleBin
在RecycleBin中与缓存相关的有三个List:
private ArrayList<View>[] mScrapViews;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
其中mTransientStateViews和mTransientStateViewsById都是缓存Transient状态的view的,而mScrapViews则是缓存Scrap状态的view。
我们从添加缓存开始来看,先来看RecycleBin的addScrapView函数,部分代码如下:
void addScrapView(View scrap, int position) {
...
// 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);
}
}
}
在这里我们先判断view是否处于Transient状态,如果是Transient,则将其保存至mTransientStateViews或mTransientStateViewsById中。至于到底保存到哪个list中,则通过mAdapterHasStableIds变量来判断,mAdapterHasStableIds则是通过Adapter的hasStableIds获得的,这个函数是需要子类去实现,它的含义是Adapter拥有稳定的ItemId,即Adapter中同一个Object的ItemId是固定不变的,这就需要我们一定要重写Adapter的getItemId方法,否则这里就会出现问题。
关于ItemId这部分,在AbsListView的setItemViewLayoutParams可以查看到:
private void setItemViewLayoutParams(View child, int position) {
final ViewGroup.LayoutParams vlp = child.getLayoutParams();
...
if (mAdapterHasStableIds) {
lp.itemId = mAdapter.getItemId(position);
}
lp.viewType = mAdapter.getItemViewType(position);
if (lp != vlp) {
child.setLayoutParams(lp);
}
}
回到addScrapView函数,如果不是Transient状态,则会将child保存到mScrapViews中。
3、obtainView
前面我们看到了添加缓存的过程,那么在哪里使用呢?
obtainView函数是AbsListView的一个函数,用于获取每个item的view,其中就包括使用缓存机制。
这个函数的代码如下:
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);
}
}
isScrap[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 {
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
...
return child;
}
这里面包含两个部分
第一部分:
通过RecycleBin的getTransientStateView获取transient状态的view。
如果存在对应position的transient状态的view,再判断transientView的viewType与这个position的ViewType是否一致。
如果ViewType一致,则调用Adapter的getView方法获取child,而transientView作为convertView参数。
如果得到的child的view与transientView不是同一个对象,比如getView中未使用convertView,则将child添加进ScrapView缓存中。
第一部分结束直接return了,不会继续执行下一部分。
第二部分:
如果不存在transient状态的view,即getTransientStateView获取的是null,那么通过RecycleBin的getScrapView函数从缓存列表中获取一个scrapView。
注意这里没有判断ViewType,是因为getScrapView函数内部进行判断处理了。
然后调用Adapter的getView方法获取child,而将scrapView作为convertView参数。
最后同样判断得到的child的view与scrapView是不是同一个对象,不是则添加进ScrapView缓存。
4、getView的调用
以上就是ListView和GridView的缓存机制。
那么我们来思考另外一个问题:
经常使用Adapter的同学可能会发现,当初始化页面的时候,getView的调用并不是从0到count走一遍即可。那么为什么会这样?这样的意义在哪?
这就要从ListView和GridView的measuer说起。
5、GridView的onMeasure
先来看看GridView的onMeasure方法,关键代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
final int count = mItemCount;
if (count > 0) {
final View child = obtainView(0, mIsScrap);
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
...
child.measure(childWidthSpec, childHeightSpec);
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (mRecycler.shouldRecycleViewType(p.viewType)) {
mRecycler.addScrapView(child, -1);
}
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
int ourSize = mListPadding.top + mListPadding.bottom;
final int numColumns = mNumColumns;
for (int i = 0; i < count; i += numColumns) {
ourSize += childHeight;
if (i + numColumns < count) {
ourSize += mVerticalSpacing;
}
if (ourSize >= heightSize) {
ourSize = heightSize;
break;
}
}
heightSize = ourSize;
}
...
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
当GridView有Adapter且其count>0时,通过obtainView这个函数获取到了position为0的child。
在这里就解释来getView的调用问题,因为通过前面内存我们知道obtainView函数中调用了getView,所以对于GridView来说position为0的getView会提前被调用一次。
那么这里为什么要得到这个child?
我们继续向下看,拿到child之后收到调用了它的measure函数进行自身测量,然后拿到child的高度MeasuredHeight。
继续向下看,当height的SpecMode为UNSPECIFIED或AT_MOST时,则需要用这个child的MeasuredHeight去计算GridView的高度。
当SpecMode为UNSPECIFIED时,GridView的高度只是一个child的高度,这就是为什么在ListView或ScrollView中嵌套GridView只显示一行的原因。
当SpecMode为AT_MOST,需要考虑GridView的ColumnNum,GridView的高度实际上是第一个child的高度和rowNum的乘积,并且加上垂直方向的间隔mVerticalSpacing。
上面的嵌套情况,我们一般的做法是将GridView完全撑开,即自定义一个GridView并重写onMeasuer方法,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
这种情况下正是SpecMode为AT_MOST的情况,注意这时的GridView的撑开的高度只与第一个child的高度有关!
6、ListView的onMeasure
上面我们研究了GridView的onMeasure,下面来看看ListView的onMeasure,代码如下:
@Override
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);
// 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);
}
}
...
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;
}
同样,当有Adapter且其count>0时,通过obtainView这个函数获取到了第一个child。
然后我们没有看到child的measure函数,但是执行了一个measureScrapChild函数,这个函数中对child进行了一次measure,这里就不贴出代码了。
在高度计算方面,SpecMode为UNSPECIFIED时与GridView一样,这也解释了ScrollView或ListView嵌套ListView为啥只显示一行。
与GridView一样,解决嵌套问题也是自定义ListView并重写onMeasure方法。
但是这里SpecMode为AT_MOST的情况与GridView有所不同,我们看到执行了一个measureHeightOfChildren函数,这个函数代码如下:
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}
...
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
...
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
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();
...
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight;
}
当Adapter不为空,这时startPosition是0,而endPosition是count-1。
再往下看,发现会遍历拿到所有的child,并通过measureScrapChild函数执行它们的measure函数。
并且将这些child的高度累加起来,同时还会加上divider的高度。
这里就与GridView有所不同了。GridView只用了第一个child去做乘积,而ListView则用到了所有child。所以当SpecMode不是AT_MOST时,ListView之后提前调用一次getView,position 是0。但是如果SpecMode是AT_MOST时,ListView先调用一次position为0的getView,然后再遍历调用一遍所有的getView,如果算上添加布局时的调用,第一个child的getView就会被调用三次!
Android进阶之路系列:http://blog.csdn.net/column/details/16488.html
网友评论