最新要闻
- 当前时讯:放假一天不调休!2023清明节假期火车票今日开售
- 快资讯丨《霍格沃茨》在线人数锐减 “反恐同”人士冷嘲热讽
- 环球今日讯!《战争机器》电影确定编剧
- 世界聚焦:刚买1个月的特斯拉掉漆车主质疑非新车 售后:交付时没问题
- 热消息:公交司机急刹车致乘客死亡获刑 官方判定当时不需刹车:交通肇事罪
- 全球最资讯丨新车品鉴:2020广州车展探馆:广汽传祺EMPOWER
- 精选!iOS 17将支持第三方应用商店:看齐安卓
- 洗衣机6个月不清洗比马桶还脏?我麻了
- 时代的眼泪 25年历史的数码相机权威网站Dpreview关停
- 每日资讯:中国移动:命苦
- 天天速看:鹿客发布掌心锁V5:掌静脉+3D结构光刷脸解锁 3399元起
- 尼泊尔空难20天就查明原因?为什么东航事故还没有结果?
- 世界微资讯!腾讯四年磨一剑!结果 让大家看笑话了
- OPPO Find X6 Pro:表演一场简单的光影魔术
- 亨迪药业(301211):3月21日北向资金减持66.63万股
- 世界热文:全明星爆款APP“偶像驾到”正式上线 主播经济和明星经纪开启新时代
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
世界热议:Android性能优化-ListView自适应性能问题
作者:京东物流 张振勇
(资料图片)
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也被执行两次,那为何不是调用5x2+5x2=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 = FrameLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = LinearLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = RelativeLayoutView onMeasure 4次 onLayout 2次 onDraw 1次
2层嵌套:A = FrameLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = LinearLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = RelativeLayoutView onMeasure 8次 onLayout 2次 onDraw 1次
3层嵌套:A = FrameLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = LinearLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = RelativeLayoutView onMeasure 16次 onLayout 2次 onDraw 1次
4层嵌套:A = FrameLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = LinearLayoutView onMeasure 2次 onLayout 2次 onDraw 1次A = RelativeLayoutView 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的次数为2n+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
关键词:
-
世界热议:Android性能优化-ListView自适应性能问题
ListView是Android中最常用的视图之一,使用的频率仅仅次于几大基础布局。但自从ListView出道至今有许多...
来源: 世界热议:Android性能优化-ListView自适应性能问题
读C#代码整洁之道笔记03_切面关注点、异常处理和线程与并发
当前时讯:放假一天不调休!2023清明节假期火车票今日开售
快资讯丨《霍格沃茨》在线人数锐减 “反恐同”人士冷嘲热讽
环球今日讯!《战争机器》电影确定编剧
世界聚焦:刚买1个月的特斯拉掉漆车主质疑非新车 售后:交付时没问题
热消息:公交司机急刹车致乘客死亡获刑 官方判定当时不需刹车:交通肇事罪
全球最资讯丨新车品鉴:2020广州车展探馆:广汽传祺EMPOWER
每日热闻!【看新股】10家拟上市银行“换道”注册制:利润普遍增长、资产质量分化、资本充足率承压
精选!iOS 17将支持第三方应用商店:看齐安卓
读Java性能权威指南(第2版)笔记24_ Java飞行记录器JFR
洗衣机6个月不清洗比马桶还脏?我麻了
时代的眼泪 25年历史的数码相机权威网站Dpreview关停
当前快报:Go 并发编程(二):锁、Select、Context、定时器
焦点报道:智能生活垃圾检测与分类系统(UI界面+YOLOv5+训练数据集)
当前通讯!Servlet的基本使用
每日资讯:中国移动:命苦
天天速看:鹿客发布掌心锁V5:掌静脉+3D结构光刷脸解锁 3399元起
尼泊尔空难20天就查明原因?为什么东航事故还没有结果?
世界微资讯!腾讯四年磨一剑!结果 让大家看笑话了
OPPO Find X6 Pro:表演一场简单的光影魔术
亨迪药业(301211):3月21日北向资金减持66.63万股
世界最资讯丨uni-app云开发入门
当前资讯!WPF学习-布局
世界热文:全明星爆款APP“偶像驾到”正式上线 主播经济和明星经纪开启新时代
小屏手机真凉了!苹果卖最差的iPhone 14 Plus 出货量也远超13 mini
今日快讯:mysql集群搭建docker-compose方案
上海农商银行完成上海市政府柜台债发售
山姆会员店寿司致多人上吐下泻:官方拒绝三倍赔偿
天天新消息丨致敬葫芦娃?保时捷911 Carrera GTS泰国特别版官图发布
环球最资讯丨14.5英寸超大屏+天玑9000!联想Y900平板正式发布:4999元起
天天观点:13代酷睿+RTX 40!联想拯救者Y7000P 2023电竞本发布:屏幕大升级
13900HX+4090顶配20999元!联想拯救者Y9000P 2023价格公布
环球最新:睡眠报告:山东人上床睡觉时间全国最早、广东人最能熬夜
焦点快看:埃安(埃)
数据库系统
世界看点:day08-2-Thymeleaf
世界快看:使用OpenCVSharp和NAudio库在C#中录制带有声音的视频并保存下来的代码
焦点快报!债市日报:3月21日
天天观速讯丨放假5天!大学生已经在为五一做准备了
外出告别“板砖”电源!联想拯救者推出140W氮化镓适配器
4799元 创维推出新款G60 4K显示器:150Hz高刷 96W反向充电
即时焦点:2023载人航天飞行任务标识正式发布:中国神话神兽鲲元素
全国车企疯狂降价!吉利汽车CEO:吉利不打价格战
每日消息!微服务与rpc/grpc
全球观热点:第132篇:npm第一次使用自己的包(package-lock.json、package.json文件作用说明)
精彩看点:【Visual Leak Detector】在 QT 中使用 VLD(方式一)
热讯:华硕发布新款Vivobook Go 14/15 OLED笔记本:高配酷睿i3-N305
天天观速讯丨真我GT Neo5 1TB存储网友用掉了500多G:直呼“真香”
全球视讯!Win12及DX13要来了?微软预告DirectX全新功能
通信能力是5G的10倍!全球17家运营商发布6G白皮书:预计2030商用
熬夜伤不起!警惕睡眠质量受电子设备影响
记录--前端加载超大图片(100M以上)实现秒开解决方案
关于“堆”题的总体思路
焦点速讯:地表最强AI,GPT-4专治各种不服
个人交340单位交680退休能拿多少?主要看个人缴纳的部分
焦点速讯:匹克态极速浪跑鞋99元抄底:门店299元
天天热文:女子公园租电动车3个多小时扣466元 运营方:上海价格都是这
焦点滚动:别羡慕了!经常不分场合秒睡可能是种病:得看医生
全球观速讯丨宁德时代麒麟电池已量产 同体积比特斯拉电池电量高13%
世界新消息丨省的都是钱 长安汽车推“百亿补贴”:不到11万可买CS75 PLUS
【Visual Leak Detector】安装 VLD
世界速递!三主摄时代来了!OPPO Find X6 Pro大漠银月图赏
全球看点:49dB降噪行业第一!OPPO Enco Free3图赏
1949元 ROG魔方幻“月曜白”路由器今晚开售:三频万兆速率
今热点:微软Bing上线在线绘图功能:文字生成图片 仅支持英文
天天快资讯:安全性远超燃油车!特斯拉发布调查报告:是美国平均水平7.4倍
环球今头条!DVWA-XSS(Reflected)
环球观焦点:关于基于AWS-Cli的方式对EC2及AMI资源批量添加或者删除tag的方法
每日观点:为什么Tomcat架构要这么设计?这篇文章告诉你答案!
全球微动态丨记录监控摄像头的接入过程及web端播放
全球快资讯丨narak靶机
当前快播:新农股份: 2022年度业绩快报
环球观天下!OPPO Find X6 Pro搭载三星E6屏:亮度高达2500nit、支持Pro XDR显示
焦点滚动:用到安卓17没问题!OPPO宣布Find X系列将支持4次大版本更新
449元-6999元!OPPO Find X6发布会四大新品一图看懂
全球速递!报告:2022年中国每四辆新车就有一辆电动车 比亚迪无敌
全球焦点!为什么不建议用redis做消息队列
世界今亮点!【数位DP】计数问题
前端设计模式——状态模式
每日讯息!关于 Vue 列表渲染 key 绑定 index 的性能问题
每日观察!黄金时间·千海金:避险情绪推升金价 但本周的美联储议息会议依然关键
环球观点:杜绝虚标!哈趣投影率先启用中国CVIA亮度标准:成单片式LCD领头羊
OPPO Find N2系列赢麻了!连续三个月折叠屏销量第一
天天速看:自动洗烘拖布 石头自清洁扫拖机器人P10图赏
道奇纯燃油谢幕之作!挑战者SRT恶魔170发布:V8机械增压马力超千匹
简单讲透Mac环境下多版本python的环境变量设置,仅对小白生效
视点!"error LNK2019: 无法解析的外部符号"原因分析
今头条!IDEA Rebuild项目错误:Information:java: java.lang.AssertionError: Value of x -1
对斗破苍穹进行python文本分析
实时焦点:VsCode 常用好用插件/配置+开发Vue 必装的插件
环球简讯:爵士力克国王将湖人挤出附加赛区 迷失盐湖城小萨准三双数据难掩低迷状态
入睡妙招!研究表明穿袜子睡觉更助眠
全球热消息:AMD Zen4霸气!移动版12核心解锁130W 直追170W桌面12核心
"周杰伦演唱会门票"登顶微博热搜 14万张秒售罄
dnf机械牛和悲鸣图在哪里?DNF机械牛和悲鸣的门票分别是什么?
雨过天晴一键还原怎么用?怎么删除雨过天晴一键还原?
OA对话框打不开是怎么回事?OA对话框怎么变成普通对话框?
今日最新!脚本编写的一个通用框架
天天速讯:编写高质量c#代码的20个建议