ListView是Android中最常用的视图之一,使用的频率仅仅次于三大基础布局,虽然由于使用性和扩展性等原因备受争议,且尽管后来出现了RecyclerView的替代方案,但是ListView仍然广泛地使用在我们的项目中。
自从ListView出道至今,已经不知道衍生出了多少问题,然而很多人只关心功能功能的实现,却极少关注ListView过度调用导致的性能问题。在实际项目中,即使你正确使用了ViewHolder机制来优化ListView性能,但是在某些场景下依然会感觉卡顿严重,到底是什么原因导致的呢,我们来分析下。
1、问题演示
很多时候,我们在使用ListView的时候,都是随手写上一个layout_height=”wrap_content”或者layout_height=”match_parent”,非常常规的写法,乍一看,并没有什么问题,尤其是功能实现上也是无可挑剔。
然而,就是layout_height=”wrap_content”这个属性是导致严重的性能问题的根源,下面以一个简单的例子说明一下:
布局如上,接下来,假设ListView一共有5项,那么显示逻辑代码如下:
下面,我们来看看log打印的情况:
数一数,一个是15次getView调用,其中6次convertView为null,剩余9次convertView为复用,而ListView的数据源真正只有5项!
当然,为了场景的简单化,我们先不考虑ListView内容超过一屏幕的情况(也就是不考虑其复用机制),所以我们期待的情况应该是getView调用5次且convertView全部为null,而事实上getView多调用了10次且有一次convertView为null。
同样的,我们测试一下当layout_height=”match_parent”的情况:
另外,ListView内容超过一屏幕的情况下(考虑复用机制),测试结果一样,这里就不再演示了。
在实际项目中,Adapter的getView方法承载着大量的业务逻辑,在性能方面,除去创建视图的损耗,不正确的ListView使用方式导致的性能损耗大约是正常的3倍左右!那么到底是什么原因导致的呢?我们下面来简单分析下ListView源码。
2、原因分析
在演示了layout_height=”wrap_content”导致性能问题的现象之后,我们来从源码的角度分析下,出现这种过度调用问题的根本原因。(源码以API 23为例)
首先,layout_height=”wrap_content”属性意味着ListView的高度需要由子View决定,即在onMeasure的时候,需要一一测量子View的高度,所以我们先从其onMeasure方法入手。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
...
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
...
}
了解View绘制原理的都知道wrap_content对应的mode为MeasureSpec.AT_MOST,所以很容易就能找到测量子视图高度的代码measureHeightOfChildren,当然方法名也体现出来了,所以具体来看这个方法。
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition,
int endPosition, int maxHeight, int disallowPartialChildPosition) {
...
endPosition = (endPositio