这是我参与「第四届青训营 」笔记创作活动的第10天
性能优化
一、为什么要做性能优化
- 用户需求
- 硬件性能提升速度变缓,软件技术是独立于硬件的,所以需要在软件层面做性能优化
- ARM平台受益于架构和工艺的演进,最近几年趋势比X86平台好
- 多核程序并行化,可以利用多核提升性能,受amdal定理限制,就是多核带来的性能提升受制于我们并行的任务之间它需要去串行执行或同步执行的时间占比,顺序执行占比10%,提升幅度差距非常大
- 架构和新的材料和工艺来驱动芯片性能的进一步提升,但目前还没有成熟
- 移动处理器还受到电池技术的限制
二、性能优化是什么
1、性能优化的目标:
快:最快显示效率
稳:最佳用户体验,减少强打断
省:最低存储、最低功耗、最低流量消耗、最低计算资源
2、性能优化分类:
流畅性优化(快——响应和界面的流畅)
Google 定义:界面呈现是指从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与应用互动,应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度。 如果应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉应用不流畅,我们将这种情况称为卡顿
为什么是 60fps? 16ms 意味着 1000/60hz,相当于 60fps。这是因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。12fps 大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。24fps 使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。 24fps 是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。 但是低于 30fps 是 无法顺畅表现绚丽的画面内容的,此时就需要用到 60fps 来达到想要的效果,超过 60fps 就没有必要了。如果我们的应用没有在 16ms 内完成屏幕刷新的全部逻辑操作,就会发生卡顿
我们需要有至少每秒10-12帧来让人类大脑相信这是些图像是一个动作(motion),但此时并不流畅,24帧每秒时,人眼就会看到流畅的画面,当然其中还有运动模糊这些视觉效果的作用,这个帧率就足够流畅而且成本低,所以以前大多数电影是这个帧率,但是并没有华丽的效果,到了60帧每秒,此时不需要视觉效果的作用依然可以很流畅
如果在60fps的时候突然掉帧降至比如20fps,这时就会导致卡顿,用户会感觉到紧张和不适,所以我们需要保证帧率大约在60fps左右并且保持不变也就是需要在16ms每帧的情况下完成包括输入、计算、网络和渲染在内的所有工作,此外还有很多原因会导致超过16ms,所以性能优化就需要找到这些原因并解决
1.1、Android的线程结构
1.2、界面刷新
1.3、卡顿感的产生
1.4、解决卡顿:把耗时逻辑代码放到其他线程
1.5、如果没有vsync信号
vsync可以使屏幕实现控制数据的交换。数据交换的发生点在屏幕渲染完一帧后,所以控制数据是否交换应该由屏幕决定,但是计算机五大组成部分各司其职,屏幕只是输入输出设备(因为能触屏),不是控制器,所以需要借助vsync来控制数据的交换
资源优化(省——最小负载带来的最大收益)
2.1、资源:Android手机的软件和硬件资源,通俗应用依赖的移动终端的有限资源和系统设置的数值,即功耗、存储、流量、系统参数、CPU、内存等
2.2、Android端能做哪些资源类优化
资源优化:
-
端侧资源:
- 功耗:深/黑色功耗小
- 内存
- 存储
- CPU
- GPU
- 网络
- 音量
- 亮度
-
服务侧资源:
- CDN带宽
- API流量
稳定性优化(稳——稳定的实现。减少不必要打断)
3.1、稳定性(Stability)
-
崩溃(Crash)
-
超时(Timeout)
- 卡顿掉帧(UI Jank)
- 应用程序无响应(ANR)
系统级优化(拓展——底层booster)
4.1、移动操作系统和硬件厂商的性能优化
Android平台 对性能优化影响比较大的feature:
zram&ksm:内存提升
render script:GPU加速
新虚拟机ART
HMP大小核调度,出现8核处理器,对外置存储有提升
Cpuset:前后台应用context ,帮助大小核使用
Vulkan引入:替代OpenGL图形库
art speed profile 和image profile
binder重构,binder并发性能得到大幅提升
可以通过google play下发speed profile EAS大小核调度器引入解决功耗和性能平衡的痛点
2019年引入分代的并行拷贝GC,提升GC性能
4.2、理解虚拟机,核心库,kernel,芯片soc这些模块怎么作用到我们的程序的,由此利用这些模块来实现性能优化
三、如何优化
1、耗时拆解
-
CPU Time:指占用CPU进行计算所花费的时间绝对值,中断、挂起、休眠等行为是不会增加CPU Time的,所以因CPU Time开销占比高导致的不合理耗时点往往是逻辑本身复杂冗长需要消耗较多cpu时间片才能处理完。
- 常见的高CPU占用是循环,比如抖音启动时遇到过一个so加载耗时,最后定位原因是在解压so的时候,遍历ZipEntry的次数过多导致,一个可行的优化策略就是可以把so所在的ZipEntry提前,遍历完so的ZipEntry之后可以提前中止遍历,而不需要遍历剩下的无效ZipEntry。
-
CPU Schedule:主要针对主线程,指主线程处于可执行状态但获取不到cpu时间片,这类耗时可能和线程调度等有关,最终导致分配给主线程的cpu时间片不足以及时处理完其内任务。由于主线程的线程优先级比其他线程的优先级要高很多,通常影响并不大
- 抖音做了线上用户的启动耗时统计,这部分耗时占比不大。不过需要渲染,渲染是需要RenderThread提交GPU的渲染命令,而RenderThread并没有主线程那么高的优先级,因此比较容易受CPU的负载的影响,导致渲染耗时。
-
IO Wait :发生了IO操作需要等待IO返回结果,可能发生在读取资源和文件,类加载,甚至在内存不足时的PageFault都会导致IO Wait。
-
Lock Wait: 主要针对主线程,指其处于等锁状态,等待被其他线程唤醒或自己超时唤醒,导致这类耗时的问题种类多样,大体也是可以分为业务锁和系统锁,业务锁主要是被主线程等待的业务逻辑未能及时处理完,优化思路一般是移除主线程的锁等待逻辑或者加快被等待的业务逻辑的执行速度。系统锁主要有:String InternTable Lock,ClassLinker Lock,GC Wait Lock等
-
IPC: 进程间通信,操作系统大都含有相应的机制,Android中所特有的IPC机制是Binder,由于进行IPC调用往往需要等待通信结果本质上这也算是一种Lock Wait,但Android特有Binder机制之所以单独列出,是因为这类耗时可采用减少或替代Binder调用等手段来优化。
2、渲染分析及耗时归因
一个布局从xml变成view对象的耗时点:xml IO,class反射,创建view的measure、layout、draw,Asset资源大锁
3、View显示过程中的耗时成因
UI 构建
在UI构建阶段中首先要对界面布局的xml文件进行解析,这会导致IO Wait耗时,在接下来要解析xml文件中的TagName从而获取对应View的class会用到反射、创建各子View实例并生成View树又会用到循环递归,两部分都会增加CPU Time的开销。
UI构建的优化方案
数据绑定
然后是数据绑定阶段,该阶段主要分两部分,一部分是对数据做请求、解析、适配,另一是部分是将适配好的数据填充进UI中,前一部分往往会涉及到Json解析成Data Class实例,这里就可能涉及反射、循环遍历嵌套的数据类结构等增加CPU Time的操作。
数据绑定的优化方案
数据协议优化:json->protobuf
gson解析优化:
View显示
最后是View显示阶段,常见的measure、layout、draw三大渲染View的步骤就在其中,它们同样会产生递归遍历父子View的耗时,此外这里还涉及将应用层计算好的渲染View的数据传递给系统层做最终的像素点排布,那么必然又会产生IPC耗时
渲染和布局优化方案
系统工具查看具体的过度绘制overdraw情况,但并不能细化到具体的布局和位置,我们开发了LayerTool工具可以根据自定义需求过滤各个布局,然后在界面上提示对应的位置,可以有效的查找导致overdraw的布局。我们通过LayerTool工具查看哪些布局导致了过度绘制。然后我们可以逐个开始优化:
- 移除不必要的背景图
- 修改不合理布局
- 写高效合理的布局:随着状态的变换,及时调整布局的绘制背景
- 移除默认的 Window 背景 :Activity其实是有一层默认背景windowBackground。如果需要做到极致的优化,可以考虑将windowBackground设置为空,但是同时需要处理好随之而来的各种问题。主要包括:把windowBackground设置为空后,如果有某些View没有设置背景,就会出现花屏/重影的现象。
- 绘制层级优化
渲染优化的异步化方案
如何彻底解决主线程的阻塞问题: