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