Android性能优化-ListView自适应性能问题

作者:京东物流 张振勇
ListView是Android中最常用的视图之一,使用的频率仅仅次于几大基础布局,虽然由于使用性和扩展性等原因备受争议,且尽管后来出现了RecyclerView的替代方案,但是ListView仍然广泛地使用在我们的项目中 。
自从ListView出道至今,已经不知道衍生出了多少问题,然而很多人只关心功能功能的实现,却极少关注ListView过度调用导致的性能问题 。在实际项目中,即使你正确使用了ViewHolder机制来优化ListView性能,但是在某些场景下依然会感觉卡顿严重,到底是什么原因导致的呢,我们来分析下
1 问题演示
很多时候,我们在使用ListView的时候,都是随手写上一个layout_height=”wrap_content”或者layout_height=”match_parent”,非常常规的写法,乍一看,并没有什么问题,尤其是功能实现上也是无可挑剔 。
然而,就是layout_height=”wrap_content”这个属性是导致严重的性能问题的根源,下面以一个简单的例子说明一下:

Android性能优化-ListView自适应性能问题

文章插图
布局如上,接下来,假设ListView一共有5项,那么显示逻辑代码如下:
Android性能优化-ListView自适应性能问题

文章插图
下面,我们来看看log打印的情况:
Android性能优化-ListView自适应性能问题

文章插图
数一数,一个是15次getView调用,其中6次convertView为null,剩余9次convertView为复用,而ListView的数据源真正只有5项!
当然,为了场景的简单化,我们先不考虑ListView内容超过一屏幕的情况(也就是不考虑其复用机制),所以我们期待的情况应该是getView调用5次且convertView全部为null,而事实上getView多调用了10次且有一次convertView为null 。
同样的,我们测试一下当layout_height=”match_parent”的情况:
Android性能优化-ListView自适应性能问题

文章插图
另外,ListView内容超过一屏幕的情况下(考虑复用机制),测试结果一样,这里就不再演示了 。
在实际项目中,Adapter的getView方法承载着大量的业务逻辑,在性能方面,除去创建视图的损耗,不正确的ListView使用方式导致的性能损耗大约是正常的3倍左右!那么到底是什么原因导致的呢?我们下面来简单分析下ListView源码 。
2 ListView代码分析
在演示了layout_height=”wrap_content”导致性能问题的现象之后,我们来从源码的角度分析下,出现这种过度调用问题的根本原因 。(源码以API 29为例)
2.1 onMeasure
首先,layout_height=”wrap_content”属性意味着ListView的高度需要由子View决定,即在onMeasure的时候,需要一一测量子View的高度,所以我们先从其onMeasure方法入手 。
Android性能优化-ListView自适应性能问题

文章插图
wrap_content对应的mode为MeasureSpec.AT_MOST,所以很容易就能找测量子视图高度的代码measureHeightOfChildren,当然方法名也体现出来了,所以具体来看这个方法
Android性能优化-ListView自适应性能问题

文章插图
核心代码如上,很明显,所有的子View实例都是由obtainView方法返回的,然后再调用具体measureScrapChild来具体测量子View的高度,正常情况下这里for循环的次数就等于所有子项的个数,不过特殊的是已测量的子View高度之和大于maxHeight就直接return出循环了 。这种做法其实很好理解,ListView能显示的最大高度就是屏幕的高度,如果有1000个子项,前面10项已经占满了一屏幕了,那后面的990项就没必要继续测量高度了,这样可以大大提高性能 。
另外,当一个子View测量完了之后,会通过recycleBin加到复用缓存之中,毕竟这个View只是测量了,还没有加到视图树之中,完全是可以继续复用的 。
继续来看obtainView方法的实现,源码在AbsListView中 。
Android性能优化-ListView自适应性能问题

文章插图
obtainView方法里面核心的代码其实就两行,首先从复用缓存中取出一个可以复用的View,然后作为参传入getView中,也就是convertView 。
这时我们梳理一下measure过程中调用getView的全过程:
A、测量第0项的时候,convertView肯定是null的,通常需要我们Inflate一个View返回;
B、第0项测量结束,这个第0项的View就被加入到复用缓存当中了;
C、开始测量第1项,这时因为是有第0项的View缓存的,所以getView的参数convertView就是这个第0项的View缓存,然后重复B步骤添加到缓存,只不过这个View缓存还是第0项的View;


推荐阅读