相信很多人在使用自定义Adapter的时候都遇到这样的问题:

假设Adapter数据源中只有30个Item,理论上每显示一个新的Item的时候就会调用一次getView,均显示一次的话是要调用getView() 30次的,然而当我们在getView输出Log信息时,前几个会被重复多轮调用,之后每滑动到一个新的Item便会正常调用getView?

针对这个问题,网上很多帖子指明这与ListView的Item的高度计算方法有关,并强调解决该问题的办法是在XML文件里面定义listView的时候需要设置height为fill_parent或者是指定的高度值,不仅仅是ListView,甚至ListView的父控件也要是fill_parent或指定高度。那么问题来了,ListView的Item的高度计算方法究竟是如何影响getView的调用的呢?

既然是计算ListView的高度,那一定少不了onMeasure过程,所以我们首先来分析ListView.onMeasure源码:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
... int childWidth = 0;
// 默认为0
int childHeight = 0;
int childState = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
// ViewMode 处于UNSPECIFIED 状态,且mAdapter.getCount() > 0时绘制首项来探测大小
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
heightMode == MeasureSpec.UNSPECIFIED)) {
// getView(0)
final View child = obtainView(0, mIsScrap); measureScrapChild(child, 0, widthMeasureSpec); childWidth = child.getMeasuredWidth();
// 更新为item0的高度
childHeight = child.getMeasuredHeight();
...
}
...
/*
* ViewMode 处于UNSPECIFIED 状态,当mItemCount > 0时,heightSize为getView(0)的高度+ListView距离顶部和底部的距离+2*getVerticalFadingEdgeLength;
* 当mItemCount==0时,childHeight=0,则heightSize返回的仅仅是ListView距离顶部和底部的距离+2*getVerticalFadingEdgeLength
*/
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
// ViewMode 处于AT_MOST 状态,绘制多个列表项以确定高度。
if (heightMode == MeasureSpec.AT_MOST) {
// 会调用多个getView,这些view将不会被复用
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
} setMeasuredDimension(widthSize , heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}

从以上源码及注释,我们得到三条信息:

(1)对于测量规格为UNSPECIFIED 的情形,若mAdapter.getCount() > 0时绘制首项来探测大小,最后返回首项高度+ListView的某些高度属性;若mAdapter.getCount() = 0或mAdapter==null时仅返回ListView的某些高度属性。

(2)对于测量规格为AT_MOST的情形,会调用measureHeightOfChildren,该方法会调用多个getView去探测ListView高度,并且探测过程中inflate生成的ItemView最后不会被回收,稍候我们会分析,正是measureHeightOfChildren方法的多次调用导致重复多轮调用getView。

(3)对于测量规格为EXACTLY的情形,源码中没有if分支判断,那就是默认执行到最后,也就是直接设置heightSize为测量规格中的size,对于这种情形,没有涉及getView的调用,是最高效的。

讲到这里,我们其实很清楚问题的根源是在ListView.measureHeightOfChildren这个方法,该方法的第35行执行heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);这里重点说明一下传入的参数,第二个参数为探测的起始位置startPosition,这里传入0,第三个参数为探测的结束位置,这里传入NO_POSITION,该值为ListView类的常数-1,第四个参数为最大高度,也就是父控件强加给ListView的高度限制。下面我们来阅读一下measureHeightOfChildren方法的源码:

 final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
final int maxHeight, int disallowPartialChildPosition) { final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}
int returnedHeight = mListPadding.top + mListPadding.bottom;
...
View child;
// onMeasure传递过来的endPosition==NO_POSITION,也就是-1,则令endPosition取数据源的adapter.getCount() - 1
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;
/*
* 从起始位置0(onMeasure中传入的是0)开始,循环地创建ItemView,并累加ItemView的高度,当高度和超过onMeasure传递过来的maxHeight(其实是测量规格中的size)时,跳出循环
*/
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec); if (i > 0) {
// 计算高度的时候还需将ItemView间的分隔距离考虑进来
returnedHeight += dividerHeight;
} // Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
/*
* 注意,这里addScrapView方法传入的第二个参数也就是position为-1,
* 由于Layout过程中调用getScrapView方法时传入的position>=0,
* 故position为-1的ScrapView都不会被回收,读懂这句话需要了解RecycleBin类,这里暂不深究。
*/
recycleBin.addScrapView(child, -1);
} returnedHeight += child.getMeasuredHeight();
// 循环提前终止条件
if (returnedHeight >= maxHeight) {
// 加上第i个的高度后returnedHeight已经超过了maxHeight,故高度探测应结束,说明最多只能容纳到第i个item
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个Item不能显示完全,即超出容器
? prevHeightWithoutPartialChild
: maxHeight;
} if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}
return returnedHeight;
}

通过measureHeightOfChildren第19行的循环探测,ListView能容纳多少个ItemView就能够计算出来了,在这个过程中,每执行一次循环体就会创建一个ItemView,也就是调用一次getView,整个循环算调用了一轮getView。从这里我们也能看出,在高度探测过程中,只会去探测mdapter中的前面的ItemView,也就是只会调用到position为0到i的那些ItemView。

接下来先看一下Android官方文档关于Layout的解释:

Layout is a two pass process: a measure pass and a layout pass. The measuring pass is implemented in measure(int, int) and is a top-down traversal of the view tree. Each view pushes dimension specifications down the tree during the recursion. At the end of the measure pass, every view has stored its measurements. The second pass happens in layout(int, int, int, int, int, int) and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.

When a view's measure() method returns, its getMeasuredWidth() and getMeasuredHeight() values must be set, along with those for all of that view's descendants. A view's measured width and measured height values must respect the constraints imposed by the view's parents. This guarantees that at the end of the measure pass, all parents accept all of their children's measurements. A parent view may call measure() more than once on its children. For example, the parent may measure each child once with unspecified dimensions to find out how big they want to be, then call measure() on them again with actual numbers if the sum of all the children's unconstrained sizes is too big or too small.

标红的一段英文的翻译为:一个父视图可能多次调用子视图的measure()方法。比如,父视图首先不指定大小调用子视图方法来确定需要多大尺寸,如果所有子视图尺寸总值过大或过小,会使用实际尺寸再次调用该方法。

从上面的文档来看,一趟ListView.onMeasure计算操作完毕之后,ListView的onMeasure方法返回,ListView的父视图是否接受这一测量结果还待定,若不接受可能会再次发起onMeasure测量请求,因此可能会导致measureHeightOfChildren方法的多次执行,从而会导致getView的多轮重复调用,也就是position为0到i的那些ItemView会被反复多次调用。

以上过程属于个人探索,疏漏之处在所难免,如有差错,敬请批评指正。

参考文献:

视图 - View(http://blog.sina.com.cn/s/blog_4fe2ba90010080jk.html)

自定义Adapter为什么会重复多轮调用getView?——原来是ListView.onMeasure在作祟的更多相关文章

  1. Android中的自定义Adapter(继承自BaseAdapter)——与系统Adapter的调用方法一致——含ViewHolder显示效率的优化(转)

    Android中很多地方使用的是适配器(Adapter)机制,那我们就要好好把这个Adapter利用起来,并且用出自己的特色,来符合我们自行设计的需要喽~~~ 下面先上一个例子,是使用ViewHold ...

  2. 自定义adapter 的getView方法被重复执行了n次的解决方法

    1. getView执行的次数和你的getCount没有直接的关系   ,getCount和你listView里面的条目数量(行数量)有关系 ,getView方法执行次数取决于你屏幕上显示几个条目,比 ...

  3. 【转】 Pro Android学习笔记(二二):用户界面和控制(10):自定义Adapter

    目录(?)[-] 设计Adapter的布局 代码部分 Activity的代码 MyAdapter的代码数据源和构造函数 MyAdapter的代码实现自定义的adapter MyAdapter的代码继续 ...

  4. Android ListView 自定义 Adapter

    自定义Adapter类 public class ListViewAdapter extends BaseAdapter { private static final String TAG = Mai ...

  5. android 自定义adapter和线程结合 + ListView中按钮滑动后状态丢失解决办法

    adapter+线程 1.很多时候自定义adapter的数据都是来源于服务器的,所以在获取服务器的时候就需要异步获取,这里就需要开线程了(线程池)去获取服务器的数据了.但这样有的时候adapter的中 ...

  6. 关于自定义adapter使用getApplicationContext()影响主题

    最近弄了一个东西,listview+switch构成界面 关于android自定义adapter,继承自baseadapter,发现界面的switch开关主题变了想要的是浅色的主题,但是却发现变成了深 ...

  7. Android中的普通对话框、单选对话框、多选对话框、带Icon的对话框、以及自定义Adapter和自定义View对话框详解

    对话框就是一个AlertDialog,但是一个简单的AlertDialog,我们却可以将它玩出许多花样来,下面我们就来一起总结一下AlertDialog的用法.看看各位童鞋在平时的工作中否都用到了Al ...

  8. [Android] Android RecycleView和ListView 自定义Adapter封装类

    在网上查看了很多对应 Android RecycleView和ListView 自定义Adapter封装类 的文章,主要存在几个问题: 一).网上代码一大抄,复制来复制去,大部分都运行不起来,或者 格 ...

  9. Android 自定义Adapter实现多视图Item的ListView

    自定义Adapter实现多视图Item的ListView http://www.devdiv.com/adapter_item_listview-blog-20-7539.html 1.原理分析 Ad ...

随机推荐

  1. 企业级镜像管理系统Harbor

    Harbor简介 在说harbor之前,我们首先说一说直接使用docker registry的一些缺陷: 缺少认证机制,任何人都可以随意拉取及上传镜像,安全性缺失 缺乏镜像清理机制,镜像可以push却 ...

  2. 发现视口(窗口)自适应的新大陆!!vw、vh

    从事前端已经1年了,一直为背景自适应铺满整个屏幕而苦苦发愁,因为要适配不同的pc显示器,所以高度不能写死,但是写(height:100%)并不能实现,愁死我了~我要不用写好的css框架实现或者自己写j ...

  3. python中的文件操作

    文件操作时,有'r','w','a'不同的操作类型,其中'r'只能读文件,seek(),tell()函数定位读的起始地方.'w'会清空文件内容然后写文件,seek(),tell()函数定位写的起始地方 ...

  4. Java基础-Collection子接口之List接口

    Java基础-Collection子接口之List接口 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 我们掌握了Collection接口的使用后,再来看看Collection接口中 ...

  5. Node.js 被分叉出一个项目 — Ayo.js,肿么了

    (注:ayo.js叉从Node.js.目前,大量的文档仍然指向Node.js库.) ayo.js是一个JavaScript运行时建立在Chrome的V8 JavaScript引擎.ayo.js使用事件 ...

  6. 安装VisualSVN Server 报错The specified TCP port is occupied

    安装过程中报错,如下图所示. The specified TCP port is occupied by another service.Please stop that service or use ...

  7. mongodb 跟踪SQL语句及慢查询收集

    有个需求:跟踪mongodb的SQL语句及慢查询收集 第一步:通过mongodb自带函数可以查看在一段时间内DML语句的运行次数. 在bin目录下面运行  ./mongostat -port 端口号  ...

  8. 使用abcpdf将html转换成pdf文件

    ABCpdf.NET使用介绍 最新做一个项目需要生成pdf文档以供打印,研究决定使用abcpdf这款组件,先针对其使用方法做一个简单的总结介绍以给有需要的朋友做参考. 一. ABCpdf.NET简单介 ...

  9. bootstrap-tooltip+validate

    名称 返回类型 描述 validate(options) 返回:Validator 验证所选的FORM valid() 返回:Boolean 检查是否验证通过 rules() 返回:Options 返 ...

  10. ArraySizeHelper解析

    [ArraySizeHelper解析] 以下代码用于获取一个数组的元素个数,例如 int table[100],以下宏返回100. template <typename T, size_t N& ...