Android性能优化ListView自适应性能问题
ListView是Android中最常用的视图之一,使用的频率仅仅次于几大基础布局,虽然由于使用性和扩展性等原因备受争议,且尽管后来出现了RecyclerView的替代方案,但是ListView仍然广泛地使用在我们的项目中。
自从ListView出道至今,已经不知道衍生出了多少问题,然而很多人只关心功能功能的实现,却极少关注ListView过度调用导致的性能问题。在实际项目中,即使你正确使用了ViewHolder机制来优化ListView性能,但是在某些场景下依然会感觉卡顿严重,到底是什么原因导致的呢,我们来分析下1问题演示
很多时候,我们在使用ListView的时候,都是随手写上一个layoutheightwrapcontent或者layoutheightmatchparent,非常常规的写法,乍一看,并没有什么问题,尤其是功能实现上也是无可挑剔。
然而,就是layoutheightwrapcontent这个属性是导致严重的性能问题的根源,下面以一个简单的例子说明一下:
布局如上,接下来,假设ListView一共有5项,那么显示逻辑代码如下:
下面,我们来看看log打印的情况:
数一数,一共是15次getView调用,其中6次convertView为null,剩余9次convertView为复用,而ListView的数据源真正只有5项!
当然,为了场景的简单化,我们先不考虑ListView内容超过一屏幕的情况(也就是不考虑其复用机制),所以我们期待的情况应该是getView调用5次且convertView全部为null,而事实上getView多调用了10次且有一次convertView为null。
同样的,我们测试一下当layoutheightmatchparent的情况:
另外,ListView内容超过一屏幕的情况下(考虑复用机制),测试结果一样,这里就不再演示了。
在实际项目中,Adapter的getView方法承载着大量的业务逻辑,在性能方面,除去创建视图的损耗,不正确的ListView使用方式导致的性能损耗大约是正常的3倍左右!那么到底是什么原因导致的呢?我们下面来简单分析下ListView源码。2ListView代码分析
在演示了layoutheightwrapcontent导致性能问题的现象之后,我们来从源码的角度分析下,出现这种过度调用问题的根本原因。(源码以API29为例)2。1onMeasure
首先,layoutheightwrapcontent属性意味着ListView的高度需要由子View决定,即在onMeasure的时候,需要一一测量子View的高度,所以我们先从其onMeasure方法入手。
wrapcontent对应的mode为MeasureSpec。ATMOST,所以很容易就能找测量子视图高度的代码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中的情况是position0,convertViewnull,而position1,2convertView都是同一个对象实例,即被复用第0项。2。2Layout
当Measure过程结束了,下面就要开始Layout过程了,由于onLayout方法代码较多,我们直接pass,来看makeAndAddView方法,也就是真真创建View的代码。
同样的,子View实例都是由obtainView方法返回的。这时候就有个小细节了,由于前面Measure的时候,第0项的View已经创建了并且加入到了复用缓存当中,这一次就可以直接拿出来继续用了。接着创建第1,2后面项的时候就没复用缓存了,只能一次次地Inflate。
所以,我们log中的情况是position0,convertView复用第0项,而position1,2convertViewnull。
按理说,Layout之后,应该就不会在调用getView方法了,但是我们明显能看到log仍然多了5次调用,那么这又是怎么回事呢?
前面说到onMeasure方法会导致getView调用,而一个View的onMeasure方法调用时机并不是由自身决定,而是由其父视图来决定。
ListView放在FrameLayout和RelativeLayout中其onMeasure方法的调用次数是完全不同的。2。3小结
由于onMeasure方法会多次被调用,例子中是两次,其实完整的调用顺序是onMeasureonLayoutonMeasureonLayoutonDraw。所以我们又会看到5次调用,和最前面5次是一模一样的。
那么,肯定有童鞋又要问,既然onLayout也被执行两次,那为何不是调用5x25x220次呢?
在第2次onLayout的时候,由于数据并没有变化,即mDataChangedfalse,这时候可以直接用当前项已经存在的View了,不要再通过getView方法重新绑定数据,所以getView是不需要被调用的。
从上面的分析中,我们可以得到wrapcontent情况下getView被调用的时机和次数,假设onMeasure(heightMeasureSpec为ATMOST)次数为n,onLayout次数为m,ListView控件内同时显示的子项数为i,那么getView次数(n1)i,正常情况matchparent时,getView次数i,多余的getView调用次数应该是(n1)iini;
由公式可以看出getView多余调用次数与onMeasure次数n以及显示子项数i成正比关系。3三大基础布局性能比较
1层嵌套:
AFrameLayout
ViewonMeasure2次onLayout2次onDraw1次
ALinearLayout
ViewonMeasure2次onLayout2次onDraw1次
ARelativeLayout
ViewonMeasure4次onLayout2次onDraw1次
2层嵌套:
AFrameLayout
ViewonMeasure2次onLayout2次onDraw1次
ALinearLayout
ViewonMeasure2次onLayout2次onDraw1次
ARelativeLayout
ViewonMeasure8次onLayout2次onDraw1次
3层嵌套:
AFrameLayout
ViewonMeasure2次onLayout2次onDraw1次
ALinearLayout
ViewonMeasure2次onLayout2次onDraw1次
ARelativeLayout
ViewonMeasure16次onLayout2次onDraw1次
4层嵌套:
AFrameLayout
ViewonMeasure2次onLayout2次onDraw1次
ALinearLayout
ViewonMeasure2次onLayout2次onDraw1次
ARelativeLayout
ViewonMeasure32次onLayout2次onDraw1次
从上面逻辑可以看出,RelativeLayout会导致子View的onMeasure重复调用,假设嵌套层数为n,子View的onMeasure次数为2(n1),如果onMeasure中做了复杂逻辑,将会容易导致卡顿。
另外,如果上面的子View是ListView,且如果高度设置为wrapcontent,恰好一屏幕的item个数是m,那么其adapter的getView方法调用次数(2n1)m。假设n4,m10,getView170次!170次!170次!(为何会这样,下回合分解,有时间的可以先去玩下,)
所以,三大布局对子View的影响排名应该是:
LinearLayoutFrameLayoutRelativeLayout4常见错误4。1常见错误1
比如4层嵌套的RelativeLayout会使得子View的onMeasure次数达到32,其中heightMeasureSpec为ATMOST的次数为16,所以如果ListView同时显示的项数为10,那么getView的次数达到(161)10170次,虽然只有10项,但是却相当于一次性加载了170项,性能损耗之大可想而知。
可以总结出一个公式:如果RelativeLayout嵌套层数为n,ListView显示项数为m,getView调用次数为(2n1)m4。2常见错误2
从官方的设计来看,ListView其实是禁止防止在ScrollView等垂直滚动视图中的,但无奈各种各样的业务和设计导致我们不得不这么做,然后就衍生出了可谓ListView历史上最大的坑:NoScrollListView。
NoScrollListview出现的主要目的是为了支持ListView放在ScrollView等垂直滚动视图中,原理很简单,利用前面ListView测量原理分析到的机制,强行设置ATMOST来测量子View高度,也就是强制ListView自适应,即使你在xml中正确地使用layoutheightmatchparent,在Java代码里面也会强行设置成wrapcontent,导致的结果就是每一次onMeasure都会不停调用getView。
如果,结合上前面说的RelativeLayout嵌套,ListView的性能损耗还要再翻倍!
假设ScrollView中存在RelativeLayout里面嵌套NoScrollListview,RelativeLayout嵌套层数为n,那么onMeasure的次数为2n2(n1)次,ListView显示项数为m,getView调用次数为(2n2(n1)1)m次。如果n4,m10,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的时候注意尽量使用layoutheightmatchparent。如果第1点无法避免,需要注意ListView的父布局,父布局以上绝对不要使用RelativeLayout,即使使用FrameLayout或LinearLayout会增加布局层级。如果第1点无法避免,需要注意不要在getView中使用setVisibility这种会触发ListView重新onMeasure的操作。如果ListView存在位移,比如下来刷新等,绝对要遵循第1点来设置layoutheightmatchparent,不然频繁触发onMeasure会导致交互卡顿。关于NoScrollListView,这种布局是严禁使用的,无论是哪种场景,如果ScrollView中必须要使用ListView,可以使用SimulateListView控件代替ListView
养老金重算补发开始,退休人员统一补发10个月的养老金吗?养老金重算补发开始,退休人员统一补发10个月的养老金吗?时间来到11月份,距离2022年结束已经只有不到两个月的时间了。对于退休人员来说,11月份又是一个比较关键的月份,因为本月迎
工龄不到35年,养老金能达到3000元以上吗?怎么算的?点击上方蓝色按钮,即可收听全文。工龄不到35年,养老金能达到3000元以上吗?怎么算的?工龄在一定程度上反映了一个人对社会对单位的贡献程度,按照我国目前执行的退休年龄,男性60岁,
四川遂宁民企一哥力压天齐锂业,年进账253。42亿,还没上市11月财经新势力遂宁,四川省地级市,别称斗城,成渝经济区区域性中心城市,四川省的现代产业基地,以养心文化为特色的现代生态花园城市。位于四川盆地中部,涪江中游,是成渝经济区和成都平原
重庆资本玩家的不归路前有恒大集团的许家印负债20000亿陷入困境,后有隆鑫老板涂建华负债430个亿。这些老板们都是在自己的主业,做到了顶端,行业遇到了困境,为了寻求突破,他们都转型做房地产。房地产的利
通过炒股实现财务自由的人,都做对了什么?靠股票赚到钱的人,只有这三种人!在中国股市上,普通人很难赚到钱,而靠炒股能改变命运的人有三大类人第一类,以徐翔为代表的投资者,成立私募基金,然后他们利用资金优势信息优势,来拉升个股
世界杯开打,A股为何每次遇到世界杯总会回调?今天低开幅度比较大的行情是有些出乎意外的,周末没什么利空,周边股市表现的都比较稳定,这种大幅低开只能说明下面3048那个缺口有点太膈应人了,全市场都在盯着它,索性还不如无限靠近它一
再跌破1。6万!赵长鹏比特币没有死以太坊低点难交易比特币今天(21日)再度走跌,直接下探跌破1。6万美元关口,市场观点认为,上周比特币的交易区间非常狭窄,然而鉴于横向整理,预计本周将出现波动。币安创始人赵长鹏信心喊话,比特币没有死
(国际)助力周边繁荣带动当地就业走进泰中罗勇工业园成立于2006年的泰中罗勇工业园位于泰国首都曼谷东南100多公里处,是中国首批境外经贸合作区之一。截至目前,工业园已吸引了180家中国制造企业30多家配套企业在泰投资,为当地解决超
九月绍兴细雨中女儿导航找网红小吃高老太奶油小攀绍兴和我旅游思路不一样,女儿爱找网红店,我是喜欢博物馆。她将就我,也陪我去,老大不愿意。我跟着她,却也打开另一扇窗,乐于跟进。绍兴导航把我们导到偏僻小道,和鲁迅家步行街几座家属楼之
头条每日一游故宫亲爱的友友,新的一周快乐!一说起故宫,估计大家第一印象就是,故宫一个特别大的迷宫,因为我虽然去过好多次,但仍然对故宫还是一无所知,只知道好大,主路好长,建筑好壮观,好复杂,古代的劳
2022年海南美丽乡村绿色骑行活动(昌江站)在七叉镇举办为发展以人民为中心的全民健身事业,不断提升体育惠民水平,满足人民日益增长的多元化多层次的健身需求,11月20日上午,主题为山海黎乡醉美昌江的2022年海南美丽乡村绿色骑行活动(昌江