相信很多人在使用自定义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. 《剑指offer》 面试题43 n个骰子的点数 (java)

    引言:写这篇文章的初衷只是想做个笔记,因为这道题代码量有点大,有点抽象,而书上并没有详细的注释.为了加深印象和便于下次复习,做个记录. 原题:把n个骰子扔到地上,所有骰子朝上一面的点数之后为s. 输入 ...

  2. snprintf()解析

    snprintf(ssid_mac,sizeof(ssid_mac),"%s_%02X%02X",ssid,macval[4],macval[5]); ssid_mac = ssi ...

  3. python3使用stmplib发送邮件

    代码如下: import smtplib from email.mime.text import MIMEText from email.header import Header from email ...

  4. Java并发编程原理与实战二十一:线程通信wait&notify&join

    wait和notify wait和notify可以实现线程之间的通信,当一个线程执行不满足条件时可以调用wait方法将线程置为等待状态,当另一个线程执行到等待线程可以执行的条件时,调用notify可以 ...

  5. CSS3实战之box-shadow篇

    box-shadow属性包含6个参数值:阴影类型.X轴位移.Y轴位移.阴影大小.阴影扩展和阴影颜色.这6个参数值可以有选择地省略. 现在我们用一个img元素来举栗子 我们先来写最简单的box-shad ...

  6. 线搜索(line search)方法

    在机器学习中, 通常需要求某个函数的最值(比如最大似然中需要求的似然的最大值). 线搜索(line search)是求得一个函数\(f(x)\)的最值的两种常用迭代方法之一(另外一个是trust re ...

  7. redis写定时任务获取root权限

    前提: 1.redis由root用户启动. 2.开启cron的时候,/var/spool/cron linux机器下默认的计划任务,linux会定时去执行里面的任务. 启动服务 :/sbin/serv ...

  8. centos7.2安装php7.2

    Centos 7源码编译安装 php7.2 原文地址:https://renwole.com/archives/29 介绍: 先安装php依赖包,否则在编译安装php7的过程当中会出现各种报错,安装完 ...

  9. 添加 MySql 服务、Tomcat服务到windows服务中

    添加 MySql 服务到windows服务中: cmd --> F:\MySql\MySqlServer5.1\bin\mysqld --install 这样用默认的 MySQL 为名称添加一个 ...

  10. RTSP消息交互过程

    c表示客户端,s表示RTSP服务器端 第一步:查询服务器可用方法 1 c---s :OPTION request //查询s有哪些方法可用 s---c:OPTION response //s回应信息的 ...