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

相信很多人在使用自定义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源码:

 1 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 2     ...
 3
 4     int childWidth = 0;
 5     // 默认为0
 6     int childHeight = 0;
 7     int childState = 0;
 8
 9     mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
10     // ViewMode 处于UNSPECIFIED 状态,且mAdapter.getCount() > 0时绘制首项来探测大小
11     if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
12             heightMode == MeasureSpec.UNSPECIFIED)) {
13         // getView(0)
14         final View child = obtainView(0, mIsScrap);
15
16         measureScrapChild(child, 0, widthMeasureSpec);
17
18         childWidth = child.getMeasuredWidth();
19         // 更新为item0的高度
20         childHeight = child.getMeasuredHeight();
21         ...
22     }
23     ...
24     /*
25      *  ViewMode 处于UNSPECIFIED 状态,当mItemCount > 0时,heightSize为getView(0)的高度+ListView距离顶部和底部的距离+2*getVerticalFadingEdgeLength;
26      *  当mItemCount==0时,childHeight=0,则heightSize返回的仅仅是ListView距离顶部和底部的距离+2*getVerticalFadingEdgeLength
27      */
28     if (heightMode == MeasureSpec.UNSPECIFIED) {
29         heightSize = mListPadding.top + mListPadding.bottom + childHeight +
30                 getVerticalFadingEdgeLength() * 2;
31     }
32     // ViewMode 处于AT_MOST 状态,绘制多个列表项以确定高度。
33     if (heightMode == MeasureSpec.AT_MOST) {
34         // 会调用多个getView,这些view将不会被复用
35         heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
36     }
37
38     setMeasuredDimension(widthSize , heightSize);
39     mWidthMeasureSpec = widthMeasureSpec;
40 }

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

(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方法的源码:

 1 final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
 2             final int maxHeight, int disallowPartialChildPosition) {
 3
 4         final ListAdapter adapter = mAdapter;
 5         if (adapter == null) {
 6             return mListPadding.top + mListPadding.bottom;
 7         }
 8         int returnedHeight = mListPadding.top + mListPadding.bottom;
 9         ...
10         View child;
11         // onMeasure传递过来的endPosition==NO_POSITION,也就是-1,则令endPosition取数据源的adapter.getCount() - 1
12         endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
13         final AbsListView.RecycleBin recycleBin = mRecycler;
14         final boolean recyle = recycleOnMeasure();
15         final boolean[] isScrap = mIsScrap;
16         /*
17          * 从起始位置0(onMeasure中传入的是0)开始,循环地创建ItemView,并累加ItemView的高度,当高度和超过onMeasure传递过来的maxHeight(其实是测量规格中的size)时,跳出循环
18          */
19         for (i = startPosition; i <= endPosition; ++i) {
20             child = obtainView(i, isScrap);
21
22             measureScrapChild(child, i, widthMeasureSpec);
23
24             if (i > 0) {
25                 // 计算高度的时候还需将ItemView间的分隔距离考虑进来
26                 returnedHeight += dividerHeight;
27             }
28
29             // Recycle the view before we possibly return from the method
30             if (recyle && recycleBin.shouldRecycleViewType(
31                     ((LayoutParams) child.getLayoutParams()).viewType)) {
32                 /*
33                  *  注意,这里addScrapView方法传入的第二个参数也就是position为-1,
34                  *  由于Layout过程中调用getScrapView方法时传入的position>=0,
35                  *  故position为-1的ScrapView都不会被回收,读懂这句话需要了解RecycleBin类,这里暂不深究。
36                  */
37                 recycleBin.addScrapView(child, -1);
38             }
39
40             returnedHeight += child.getMeasuredHeight();
41             // 循环提前终止条件
42             if (returnedHeight >= maxHeight) {
43                 // 加上第i个的高度后returnedHeight已经超过了maxHeight,故高度探测应结束,说明最多只能容纳到第i个item
44                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
45                             && (i > disallowPartialChildPosition) // We‘ve past the min pos
46                             && (prevHeightWithoutPartialChild > 0) // We have a prev height
47                             && (returnedHeight != maxHeight) // 第i个Item不能显示完全,即超出容器
48                         ? prevHeightWithoutPartialChild
49                         : maxHeight;
50             }
51
52             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
53                 prevHeightWithoutPartialChild = returnedHeight;
54             }
55         }
56         return returnedHeight;
57     }

通过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)

时间: 2024-08-10 02:11:03

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

自定义adapter的基础上Listview优化方案以及几个小错误(checkbox吃掉点击事件以及对象重复问题)

每次adapter运行都有一个getcount,有多少条就调用多少次getview,就会解析多少次xml文件(创建view,条数多了很消耗时间),13年谷歌提出了一个机制,每次只缓存一屏幕多几个,把划出屏幕外的listview回收(用的convertView),只要修改里面的值就可以重新用不用再创建一个view 修改的是自定义adapter中的getview方法 @Overridepublic View getView(int position, View convertView, ViewGr

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

Android中很多地方使用的是适配器(Adapter)机制,那我们就要好好把这个Adapter利用起来,并且用出自己的特色,来符合我们自行设计的需要喽~~~ 下面先上一个例子,是使用ViewHolder进行显示效率优化过的工程: package com.test.listviewsimpleadapter;    import java.util.ArrayList;  import java.util.HashMap;  import java.util.List;  import java

自定义ListView实现中间项动态变大的效果(不是自定义Adapter)

为什么强调不是自定义Adapter,因为我这个自定义控件是来源与公司新做的项目,刚开始在百度上找了一圈,都说是自定义ListView ,点进去却是自定义Adaper,有的人就会说你是不是太较真了,自定义Adapter就基本可以实现各种效果了,何必要自定义Listview,今天我做的这个还确实不好用Adapter做,先上效果图,右边的动图来源于左边这个项目中的一个控件. 因为我们的项目中,六个通道的检测过程要同时动态显示,这样位置就要合理调配,因为六个通道采用的布局比较相似,所以当时考虑了Frag

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

目录(?)[-] 设计Adapter的布局 代码部分 Activity的代码 MyAdapter的代码数据源和构造函数 MyAdapter的代码实现自定义的adapter MyAdapter的代码继续探讨BaseAdapter 我们可以同继承抽象类BaseAdapter来实现自己的Adapter,自己设置子View的UI,不同子View可以由不同的布局,并自己进行数据和子view中数据的对应关系.图是例子的呈现结果,我们有很多图标,对这些图标按一定大小进行缩放,然后布局在GridView中.这个

自定义Adapter中实现startActivityForResult的分析

最近几天在做文件上传的时候,想在自定义Adapter中启动activity时也返回Intent数据,于是想到了用startActivityForResult,可是用mContext怎么也调不出这个方法,只能调用startActivity这个方法,于是在网上搜一下,可以利用一个方式可以间接的解决这个问题,果断贴代码: Intent mIntent = new Intent(mContext,clazz);((Activity) mContext).startActivityForResult(mI

Android学习----自定义Adapter实现ListView

前言: 对于ListView而言,自定义的Adapter对于显示复杂的界面有很大的灵活性 .使用自定义的Adapter需要继承BaseAdapter,然后重写getCount(),getView(),getItem,getItemId()4个方法.adapter在绘制listview时是先根据getCount()获得底层数据的个数来判断绘制item的个数,然后通过getView绘制单个item. ListView实现的效果如下: 详细步骤: 1.新建Activity,在对应的布局文件中放置lis

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

adapter+线程 1.很多时候自定义adapter的数据都是来源于服务器的,所以在获取服务器的时候就需要异步获取,这里就需要开线程了(线程池)去获取服务器的数据了.但这样有的时候adapter的中没有数据. 如下面的代码: 这就是在initData中异步获取服务器的数据,然后实例化adatper,再将adapter赋给listView. 2.initData()中的代码是: 这里线程要睡眠5秒钟,是为了模仿网络的耗时操作 3.Handler: 在Handler中接收到数据后给list赋值后,

ListView中使用自定义Adapter及时更xin

在项目中,遇到不能ListView及时更新的问题.写了一个demo,其中也遇到一些问题,一并写出来.好吧,上代码: public class PersonAdapter extends BaseAdapter { private ArrayList<PersonBean> mList; private Context mContext; public PersonAdapter(ArrayList<PersonBean> list, Context context) { mList

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

对话框就是一个AlertDialog,但是一个简单的AlertDialog,我们却可以将它玩出许多花样来,下面我们就来一起总结一下AlertDialog的用法.看看各位童鞋在平时的工作中否都用到了AlertDialog的这些特性. OK,废话不多说,进入我们今天的正题. 普通对话框 普通对话框就是我们最最常用的对话框,实现起来并不复杂,实现出来的效果当然也是最简单的,如下: AlertDialog dialog = new AlertDialog.Builder(this).setTitle("