界面优化¶
ANR¶
ANR(Application Not Responding):应用程序无响应,即卡顿。卡顿就是在应用使用过程中出现界面不响应或者界面渲染粘滞的情况。而应用界面的渲染以及事件响应是在主线程完成的,出现卡顿的原因可以归结为主线程阻塞。
造成主线程阻塞的原因可能是:
- 主线程在进行大量I/O操作:在主线程去写入大量数据;
- 主线程在进行大量计算:主线程进行复杂计算;
- 大量UI绘制:界面过于复杂,UI绘制需要大量时间;
- 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待子线程完成任务。
如果能够捕获得到卡顿当时应用的主线程调用函数堆栈,就可以知道主线程在什么函数哪一行代码卡住了,是在等什么锁,还是在进行I/O操作,或者是进行复杂计算。
卡顿监测¶
1、fps¶
通常情况下,屏幕会保持60hz/s的刷新速度,每次刷新时会发出一个屏幕刷新信号,CADisplayLink允许我们注册一个与刷新信号同步的回调处理。可以通过屏幕刷新机制来展示fps值。
利用CADisplayLink检测¶
FPS卡顿检测 屏幕刷新频率。
YYKit。利用CADisplayLink检测,CADisplayLink是一个绑定在垂直同步信号vsync的计时器。1秒调60次。
CADisplayLink绑定在mainRrunloop上,在主runloop上不断的计时。如果小于60就是卡顿了。
指标 | |
---|---|
卡顿反馈 | 卡顿发生时,fps会有明显下滑。但转场动画等特殊场景也存在下滑情况。高 |
采集精度 | 回调总是需要cpu空闲才能处理,无法及时采集调用栈信息。低 |
性能损耗 | 监听屏幕刷新会频繁唤醒runloop,闲置状态下有一定的损耗。中低 |
实现成本 | 单纯的采用CADisplayLink实现。低 |
结论 | 更适用于开发阶段,线上可作为辅助手段 |
2、ping¶
子线程与主线程通信
启动一个监控线程,监控线程每隔一小段时间(delta)ping一下主线程(发送一个dispatch_async任务到主线程),如果主线程此时有空,必然能接收到这个通知,并pong以下,如果监控线程超过delta时间没有收到pong的回复,那么可以推测UI线程必然在处理其他任务了,认为主线程已经发生卡顿。
指标 | |
---|---|
卡顿反馈 | 主线程出现堵塞直到空闲期间都无法回包,但在ping之间的卡顿存在漏查情况。中高 |
采集精度 | 子线程在ping前能获取主线程准确的调用栈信息。中高 |
性能损耗 | 需要常驻线程和采集调用栈。中 |
实现成本 | 需要维护一个常驻线程,以及对象的内存控制。中低 |
结论 | 监控能力、性能损耗和ping频率都成正比,监控效果强 |
3、利用runloop检测¶
基于runloop的检测和fps的方案非常相似,都需要依赖于主线程的runloop。由于runloop会调起同步屏幕刷新的callback,如果loop的间隔大于16.67ms,fps自然达不到60hz。而在一个loop当中存在多个阶段,可以监控每一个阶段停留了多长时间。
runloop有source timer observer事务,Observer用来监听RunLoop状态的。
runloop监听事务状态
RunLoop监控卡顿的原理如下:
如果RunLoop的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。所以,如果我们要利用RunLoop原理来监控卡顿的话,就是要关注这两个阶段。RunLoop在进入睡眠之前和唤醒后的两个loop状态定义的值,分别是kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting,也就是用来触发Source0回调和接收mach_port消息的这两个状态。
那么为什么监听kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting这两个状态而不是kCFRunLoopBeforeWaiting和kCFRunLoopAfterWaiting呢?因为RunLoop进入休眠之前(kCFRunLoopBeforeWaiting)会执行source0等方法,唤醒(kCFRunLoopAfterWaiting)后要接收mach_port消息。如果在执行source0或者接收mach_port消息的时候太耗时,那么就会导致卡顿。我们把kCFRunLoopBeforeSources作为执行Source0等方法的开始时间节点,将kCFRunLoopAfterWaiting作为接收mach_port消息的开始时间节点,所以只需要监控这两个状态是否超过设定的时间阀值。而如果监控kCFRunLoopBeforeWaiting状态,当监听到kCFRunLoopBeforeWaiting状态时,其实已经执行完了source0,无法监控source0的耗时长短,故不能监听kCFRunLoopBeforeWaiting这个状态。
主线程:
程序中的任务都是在线程中执行,而线程依赖于 RunLoop。RunLoop总是在相应的状态下执行任务,执行完成以后切换到下一个状态,如果在一个状态下执行时间过长导致无法进入下一个状态就可以认为发生了卡顿。所以可以根据主线程 RunLoop 的状态变化检测任务执行时间是否太长。至于多长时间算作卡顿可以依据自己的需要来设置,一般情况下可以设置1秒钟作为阀值。
子线程:
在子线程中处理检测任务。因为观测的是主线程的任务,把观察后的处理任务也加到主线程会使得主线程任务不纯粹,影响检测结果的准确性。
如果kCFRunLoopBeforeSources kCFRunLoopAfterWaiting两个时间段的时间太长,则是发生卡顿。
新创建一个子线程 计算主线程卡顿时间。
dispatch_semaphore_wait
使用PLCrashReporter获取堆栈信息,然后上传服务器等操作。
指标 | |
---|---|
卡顿反馈 | runloop的不同阶段把时间分片,如果某个时间片太长,则认定发生了卡顿。此外应用闲置状态常驻beforeWaiting阶段,此阶段存在误报可能。 |
采集精度 | 和fps类似的,依附于主线程callback的方案缺少准确采集调用栈的时机,但优于fps检测方案。中低 |
性能损耗 | 此方案不会频繁唤醒runloop,相较于fps性能更佳。低 |
实现成本 | 需要注册runloop observer。中低 |
结论 | 综合性能优于fps,但反馈表现不足,只适合作为辅助工具使用 |
4、stack backtrace¶
代码质量不够好的方法可能会在一段时间内持续占用CPU的资源,换句话说在一段时间内,调用栈总是停留在执行某个地址指令的状态。由于函数调用会发生入栈行为,如果比对两次调用栈的符号信息,前者是后者的符号子集时,可以认为出现了卡顿。
指标 | |
---|---|
卡顿反馈 | 由于符号地址的唯一性,调用栈比对的准确性高。但需要排除闲置状态下的调用栈信息。高 |
采集精度 | 直接通过调用栈符号信息比对可以准确的获取调用栈信息。高 |
性能损耗 | 需要频繁获取调用栈,需要考虑延后符号化的时机减少损耗。中高 |
实现成本 | 需要维护常驻线程和调用栈追溯算法。中高 |
结论 | 准确率很高的工具,适用面广 |
5、msgSend observe¶
OC方法的调用最终转换成msgSend的调用执行,通过在函数前后插入自定义的函数调用,维护一个函数栈结构可以获取每一个OC方法的调用耗时,以此进行性能分析与优化。
指标 | |
---|---|
卡顿反馈 | 高 |
采集精度 | 高 |
性能损耗 | 拦截后调用频次非常高,启动阶段可达10w次以上调用。高 |
实现成本 | 需要维护方法栈和优化拦截算法。高 |
结论 | 准确率很高的工具,但不适用于Swift代码 |
卡顿解决¶
界面优化¶
-
能异步就异步
-
拆分成小任务
60帧完成图片或uI绘制,如果没有完成则会卡顿。
init和initWithFrame¶
init 会调initWithFrame。调initWithFrame只会调initWithFrame。
1、预排版¶
谁的事情谁做,view是做视图加载。加载完数据,把数据的json放在viewModel中,把数据的行高、是否显示、富文本格式等放在model中,(高度缓存,label,imageView的高度都放在model中)。
请求网络之后,处理数据,model保存frame和其它数据,这些都在子线程中处理。所有的计算的事情都放在了子线程,提前把所有子控件的frame计算好(预排版)。
预计算 layoutModel
请求网络有了数据就可以知道高度,mode数据(frame布局 height 富文本)都可以处理,不需要等到tableView
请求完数据之后,在子线程中处理完数据,计算控件的高度frame等等,然后回到主线程reloaddata
数据不变,则对应的UI也不会变,高度不变,上滑下滑不需要多次计算。
2、预解码和下采样¶
UIImage是一个模型,不是控件,有很多属性。
二进制流dataBuffer解码imageBuffer,帧缓冲区frameBuffer渲染
拿到data之后,子线程去解码。
UIImage优化:预解码和下采样。
3、按需加载¶
滑到哪儿加载哪儿
配合占位图和缓存处理。
4、异步渲染¶
控件越来越多,图层越来越复杂,渲染难度增加。
UIView layer关系¶
UIView:交互响应事件
layer:负责渲染图层
UIView是layer的代理。
UIView和layer绑定,渲染耗时,把渲染的东西提炼出来,子线程渲染,渲染完毕,会调用layer的方法,layer发消息(displayLayer)给UIview,把layer视图显示。
事务:
- layout 构建视图
- displayer 绘制
- prepare coreAnimation
- commit 提交事务 发送给reader server