目录

前言

1、View的Transient状态

2、RecycleBin

3、obtainView

4、getView的调用

5、GridView的onMeasure

6、ListView的onMeasure


前言

在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 mListPaddingsuper.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 0heightSize = 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 childreturnedHeight += dividerHeight;}// Recycle the view before we possibly return from the methodif (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 returnedHeightreturn 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就会被调用三次!

ListView和GridView的缓存机制及measure过程相关推荐

  1. 13.ListView和GridView

    转载请标明出处:  http://blog.csdn.net/yujun411522/article/details/46226001 本文出自:[yujun411522的博客] android组件中 ...

  2. Android进阶——Android视图工作机制之measure、layout、draw

    前言 自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信.其实只要了解了View的工作机制后 ...

  3. Android视图工作机制之measure、layout、draw

    前言 自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信.其实只要了解了View的工作机制后 ...

  4. delphi listview 添加数据 慢_ListView 的缓存机制

    一.前言 ListView 作为一个 Android 5.x 之前的一个用于显示数据列表的控件,或许在今天都已经被 RecyclerView 完全替代,但是其中的缓存机制仍然值得我们去了解,对后面学习 ...

  5. 【腾讯Bugly干货分享】Android ListView与RecyclerView对比浅析--缓存机制

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/5811d... 作者:黄宁源 一,背景 RecyclerView是谷歌官方出的一 ...

  6. Android笔记(二十五) ListView的缓存机制与BaseAdapter

    之前接触了ListView和Adapter,Adapter将数据源和View连接起来,实际应用中,我们要显示的数据往往有很多,而屏幕只有那么大,系统只能屏幕所能显示的内容,当我们滑动屏幕,会将旧的内容 ...

  7. GridView实战二:使用ObjectDataSource数据源控件(自定义缓存机制实现Sort)

    参考资料:http://www.cnblogs.com/fsjohnhuang/archive/2011/12/17/2291200.html 因为使用ObjectDataSource自带的缓存机制无 ...

  8. Android学习——ListView的缓存机制

    在使用ListView的时候,需要加载适配器和数据源,这篇文章主要介绍一下ListView的使用以及利用ListView的缓存机制来减少系统的初始化时间. ListView的使用 ListView和V ...

  9. 解决ListView 缓存机制带来的显示不正常问题

    ListView加载数据原理:系统绘制ListView时,首先会用getCount()函数得到要绘制的这个列表的长度,然后开始逐行绘制.然后调用getView()函数,在这个函数里面首先获得一个Vie ...

最新文章

  1. All About Monads
  2. android之json解析优化,Android开发之json解析
  3. 数据库界的Swagger:一键生成数据库文档!你不了解一下?
  4. [导入]ASP.NET断点续传和多线程下载
  5. python调用父类对象的几个方法
  6. React 篇 Search Bar and content Table
  7. 网络七层协议 五层模型 TCP连接 HTTP连接 socket套接字
  8. java 抽象类 方法_java 抽象类
  9. 工厂方法模式--简单试例
  10. 知识整理(你想要的Linux知识都在这里)
  11. mysql 连接 分组_MySQL 基础 (四) 分组查询及连接查询
  12. smartdeblur(图片模糊处理工具) v2.2
  13. 输入汉语星期几输出英文c语言程序,c输入1234567,输出英语星期日期
  14. 测温监控摄像头_温度湿度远程监控摄像头
  15. ssci源刊里有开源期刊吗_2020年SCI期刊影响因子重磅发布!你投过的期刊涨了吗?...
  16. 游戏3D建模用到的有哪些软件?
  17. 利用3000fps把人脸面部抠出来(一)
  18. printf 输出的数据类型
  19. C++ 中vector的使用方法
  20. [置顶]援引个人新浪博客

热门文章

  1. php 时间函数参考
  2. C++学习之路—继承与派生(四)拓展与总结
  3. 轮廓处理函数详细(转)
  4. go---字符串截取
  5. mysql 基础配置经验
  6. 日常方便使用的Python脚本实现
  7. 集成电路883和883b有什么区别
  8. 窗体控件随窗体大小改变(仍有不足)
  9. ActiveMQ结合Spring收发消息
  10. 为什么有必要对网站开启https?