作者:京东物流 张振勇
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 ListView代码剖析
在演示了layout\_height=”wrap\_content”导致性能问题的景象之后,咱们来从源码的角度剖析下,呈现这种适度调用问题的根本原因。(源码以API 29为例)
2.1 onMeasure
首先,layout\_height=”wrap\_content”属性意味着ListView的高度须要由子View决定,即在onMeasure的时候,须要一一测量子View的高度,所以咱们先从其onMeasure办法动手。
wrap\_content对应的mode为MeasureSpec.AT\_MOST,所以很容易就能找测量子视图高度的代码measureHeightOfChildren,当然办法名也体现进去了,所以具体来看这个办法
外围代码如上,很显著,所有的子View实例都是由obtainView办法返回的,而后再调用具体measureScrapChild来具体测量子View的高度,失常状况下这里for循环的次数就等于所有子项的个数,不过非凡的是已测量的子View高度之和大于maxHeight就间接return出循环了。这种做法其实很好了解,ListView能显示的最大高度就是屏幕的高度,如果有1000个子项,后面10项曾经占满了一屏幕了,那前面的990项就没必要持续测量高度了,这样能够大大提高性能。
另外,当一个子View测量完了之后,会通过recycleBin加到复用缓存之中,毕竟这个View只是测量了,还没有加到视图树之中,齐全是能够持续复用的。
持续来看obtainView办法的实现,源码在AbsListView中。
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;
D、持续测量3、4、5…项,反复C。
所以,咱们log中的状况是position=0,convertView=null,而position 1,2 … convertView都是同一个对象实例,即被复用第0项。
2.2 Layout
当Measure过程完结了,上面就要开始Layout过程了,因为onLayout办法代码较多,咱们间接pass,来看makeAndAddView办法,也就是真真创立View的代码。
同样的,子View实例都是由obtainView办法返回的。这时候就有个小细节了,因为后面Measure的时候,第0项的View曾经创立了并且退出到了复用缓存当中,这一次就能够间接拿进去持续用了。接着创立第1,2 … 前面项的时候就没复用缓存了,只能一次次地Inflate。
所以,咱们log中的状况是position=0,convertView复用第0项,而position 1,2 … convertView=null。
按理说,Layout之后,应该就不会在调用getView办法了,然而咱们显著能看到log依然多了5次调用,那么这又是怎么回事呢?
后面说到onMeasure办法会导致getView调用,而一个View的onMeasure办法调用机会并不是由本身决定,而是由其父视图来决定。
ListView放在FrameLayout和RelativeLayout中其onMeasure办法的调用次数是齐全不同的。
2.3 小结
因为onMeasure办法会屡次被调用,例子中是两次,其实残缺的调用程序是onMeasure – onLayout – onMeasure – onLayout – onDraw。所以咱们又会看到5次调用,和最后面5次是截然不同的。
那么,必定有童鞋又要问,既然onLayout也被执行两次,那为何不是调用5×2+5×2=20次呢?
在第2次onLayout的时候,因为数据并没有变动,即mDataChanged=false,这时候能够间接用以后项曾经存在的View了,不要再通过getView办法从新绑定数据,所以getView是不须要被调用的。
从下面的剖析中,咱们能够失去wrap\_content状况下getView被调用的机会和次数,假如onMeasure(heightMeasureSpec为AT\_MOST)次数为n,onLayout次数为m,ListView控件内同时显示的子项数为i,那么getView次数=(n + 1)\_i,失常状况match\_parent时,getView次数= i,多余的getView调用次数应该是 (n + 1)_i – i = n * i;
由公式能够看出getView多余调用次数与onMeasure次数n以及显示子项数i成正比关系。
3 三大根底布局性能比拟
1层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 4次 onLayout 2次 onDraw 1次
2层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 8次 onLayout 2次 onDraw 1次
3层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 16次 onLayout 2次 onDraw 1次
4层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 32次 onLayout 2次 onDraw 1次
从下面逻辑能够看出,RelativeLayout会导致子View的onMeasure反复调用,假如嵌套层数为n,子View的onMeasure次数为2^(n+1),如果onMeasure中做了简单逻辑,将会容易导致卡顿。
另外,如果下面的子View是ListView,且如果高度设置为wrap_content,恰好一屏幕的item个数是m,那么其adapter的getView办法调用次数=(2^n+1)* m。假如n=4,m=10,getView=170次!170次!170次!(为何会这样,下回合合成,有工夫的能够先去玩下,^-^)
所以,三大布局对子View的影响排名应该是:
LinearLayout = FrameLayout >> RelativeLayout
4 常见谬误
4.1 常见谬误1
比方4层嵌套的RelativeLayout会使得子View的onMeasure次数达到32,其中heightMeasureSpec为AT\_MOST的次数为16,所以如果ListView同时显示的项数为10,那么getView的次数达到(16+1)\_10=170次,尽管只有10项,然而却相当于一次性加载了170项,性能损耗之大可想而知。
能够总结出一个公式:如果RelativeLayout嵌套层数为n,ListView显示项数为m,getView调用次数为(2^n+1)_m
4.2 常见谬误2
从官网的设计来看,ListView其实是禁止避免在ScrollView等垂直滚动视图中的,但无奈各种各样的业务和设计导致咱们不得不这么做,而后就衍生出了堪称ListView历史上最大的坑:NoScrollListView。
NoScrollListview 呈现的次要目标是为了反对ListView放在ScrollView等垂直滚动视图中,原理很简略,利用后面ListView测量原理剖析到的机制,强行设置AT\_MOST来测量子View高度,也就是强制ListView自适应,即便你在xml中正确地应用layout\_height=”match\_parent”,在Java代码外面也会强行设置成wrap\_content,导致的后果就是每一次onMeasure都会不停调用getView。
如果,联合上后面说的RelativeLayout嵌套,ListView的性能损耗还要再翻倍!
假如ScrollView中存在RelativeLayout外面嵌套NoScrollListview,RelativeLayout嵌套层数为n,那么onMeasure的次数为2^n+2^(n+1)次,ListView显示项数为m,getView调用次数为(2^n + 2^(n+1) +1)* m次。如果n=4,m=10,getView次数为490次
置信看到这里,终于晓得为什么ScrollView中嵌有列表的页面会卡出翔了吧!
当然,事件还远远不止这么简略,尤其在某些非凡的场景下,容易导致onMeasure频繁调用,以理论我的项目中遇到的问题场景举两个例子。
- 有些ScrollView具备下拉弹性性能,当手指下拉时会导致子View不停onMeasure,如果子View蕴含NoScrollListview,页面必定一顿一顿的。
- 如果你在getView中的某些不失当的操作导致ListView从新onMeasure,比方setVisibility为Gone等,就会造成onMeasure和getView的互相循环调用,这时候性能耗费十分重大(个别不会ANR)。
- 同样的,某些时候咱们须要监听ListView的滚动状态,会应用setOnScrollListener,因为在onMeasure的时候会触发OnScrollListener的回调,如果回调外面某些不失当的操作导致ListView再次触发onMeasure就会导致OnScrollChangeListener和onMeasure两者的死循环。
5 心得倡议
对于以上几点问题,有如下一些倡议:
- 应用ListView的时候留神尽量应用layout\_height=”match\_parent”。
- 如果第1点无奈防止,须要留神ListView的父布局,父布局以上相对不要应用RelativeLayout,即便应用FrameLayout或LinearLayout会减少布局层级。
- 如果第1点无奈防止,须要留神不要在getView中应用setVisibility这种会触发ListView从新onMeasure的操作。
- 如果ListView存在位移,比方下来刷新等,相对要遵循第1点来设置layout\_height=”match\_parent”,不然频繁触发onMeasure会导致交互卡顿。
- 对于NoScrollListView,这种布局是严禁应用的,无论是哪种场景,如果ScrollView中必须要应用ListView,能够应用SimulateListView控件代替ListView