iOS卡顿优化
iOS卡顿
卡顿的概念:
- FPS:Frame Per Second,表示每秒渲染的帧数,通过用于衡量画面的流畅度,数值越高则表示画面越流畅。
- CPU:负责对象的创建销毁、对象属性的调整、布局计算、文本计算、和排版、图片的格式转换和解码、图像的绘制(Core Graphics)。
- GPU:负责纹理的渲染(将数据渲染到屏幕)。
- 垂直同步技术:让CPU和GPU在收到vSync信号后开始准备数据,防止撕裂感和跳帧,即保证每秒输出的帧数不高于屏幕显示的帧数。
- 双缓冲技术:iOS是双缓冲机制,前帧缓存和后帧缓存,cpu计算完GPU渲染后放入缓冲区中,当gpu下一帧已经渲染完放入缓冲区,且视频控制器已经读完前帧,GPU会等待vSync(垂直同步信号)发出后,瞬间切换前后帧缓存,并让cpu开始准备下一帧数据。
卡顿监听
Runloop 监听
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//进入runloop循环
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理timer事件
kCFRunLoopBeforeSources = (1UL << 2),//即将处理source事件
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠(等待消息唤醒)
kCFRunLoopAfterWaiting = (1UL << 6),//休眠结束(被消息唤醒)
kCFRunLoopExit = (1UL << 7),//退出runloop循环
kCFRunLoopAllActivities = 0x0FFFFFFFU//集合以上所有的状态
};
原理: 卡顿是主线程进行可耗时操作,可以添加Observer到主线程的Runloop中,通过Runloop状态切换的好事,达到监控卡顿的目的。
通知Observer即将进入Runloop
Loop
-> 通知Observer即将处理事件
-> 处理事件
-> 通知Observer线程即将休眠
-> 休眠,等待被唤醒
NSRunloop调用方法是在kCFRunloopBeforeSources和kCFRunloopBeforeWaiting之间,以及kCFRunloopAfterWaiting之后,如果这两个时间内耗时太长,就可以判定出此时主线程卡顿。
所以在Runloop的最开始和结束最末尾的位置添加Observer,从而获得主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态,当主线程的状态运行超过一定的阈值,则认为主线程卡顿,从而标记为一个卡顿。
分析实现:
使用Runloop进行卡顿监控,定义一个阈值判断卡顿的出现,记录下来上报到服务器。
比如:
主程序Runloop超时的阈值是2秒,子线程的检查周期是1秒,每个1秒,子线程检查主线程的运行状态;如果检查到主线程Runloop的运行超过2秒,则认为是卡顿,并获得当前的线程快照。
假定连续5次超时50ms认为卡顿(也包含单次超时250ms)
Xcode自带的Instruments
在开发阶段,可以直接使用Instrument来检测性能问题,TimeProfiler查看与CPU相关的耗时操作,CoreAnimation查看与GPU相关的渲染操作。
比如查看离屏渲染,模拟器中选中"Debug - Color Off-screen Rendered"开启调试,真机用Instrments - Core Animation - Debug Options - Color Offscreen - Rendered Yellow开启调试,开启后,有离屏渲染的图层会变成高亮的黄色。
卡顿的原因:
iOS默认刷新频率是60HZ,所以GPU渲染只要达到60fps就不会产生卡顿。如果在60fps(16.67ms)内没有准备好下一帧数据就会使画面停留在上一帧。
只要能使CPU的计算和GPU的渲染能在规定时间内完成,就不会出现卡顿。所以目标是减少CPU和GPU的资源消耗。
卡顿造成的原因是CPU和GPU导致的掉帧引起的:
- 主线程在进行大量I/O操作:直接主线程写入大量数据
- 主线程进行大量计算:主线程进行大量复杂的计算
- 大量UI绘制:界面过于复杂,绘制UI需要大量的时间
- 主线程在等锁
优化卡顿:
CPU:
减少计算,减少耗时操作
- 提前计算好布局,列表页高度在请求完成数据后,就计算好高度,显示时直接使用。
- 尽量使用轻量级的对象,比如用不到事件处理的地方使用CALayer代替UIView
- hook setNeedsLayout、setNeedDisplay、setNeedsDisplayInRect方法,保证方法在主线程运行
- 查找因重复执行导致卡顿的方法,比如多个地方监听同一个通知,通知中执行多次的清除缓存的方法
- 保证后台运行时,不调用接口
- 把耗时的操作放到子线程
- 文本处理(尺寸计算、绘制、CoreText和YYText)
- 计算文本宽高boundingRectWithSize:options:context:和文本绘制drawWithRect:options:context放在子线程操作。
- 使用CoreText自定义文本空间,在创建对象过程中可以缓存宽高等信息,避免像UILabel/UITextView需要多次计算(调整和绘制都要计算一次),且CoreText直接使用了CoreGraphics占用内存小,效率高。(YYText)
- 图片解码:当使用UIImage或者CGImageSource创建图片时,图片数据并不会立即解码。图片设置到UIImageView或CALayer.content中,并且CALayer被提交到GPU前,CGImage中到数据才会得到解码,这一步是发生在主线程的,并且不可避免。SDWebImage处理方式:在后台线程先把图片绘制到CGBitmapmapContext中,然后直接从Bitmap创建图片。
GPU:
减少渲染
- 避免短时间内大量图片的显示,尽可能将多张图片合成一张显示
- GPU能处理的最大纹理尺寸是4096*4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
- GPU会将多个视图混合在一起再去显示,混合的过程中会消耗CPU资源,尽量减少视图数量和层次
- 减少透明的视图(alpha < 1),不透明的设置opacity为YES,GPU就不会进行alpha通道的合成
- 尽量避免出现离屏渲染
离屏渲染:
离屏渲染对GPU资源消耗极大。在OpenGL中,GPU有两种渲染方式,分别是屏幕渲染(On-Screen Rending)和离屏渲染(Off-Screen Rendering),区别在于渲染操作是在当前用于显示的屏幕缓冲区进行还是新开辟一个缓冲区进行渲染,渲染完成后再在当前显示的屏幕展示。
离屏渲染消耗性能的原因,在于需要创建新的缓冲区,并且在渲染的整个过程中,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕,造成了资源到极大消耗。
一些会触发离屏渲染的操作:
光栅化,layer.shouldRasterize = YES
遮罩,layer.mask
圆角,同时设置layer.masksToBounds=YES、layer.cornerRadius大于0,考虑通过CoreGraphics绘制裁剪圆角,或者直接使用圆角图片
阴影
画圆角避免离屏渲染:
CAShapeLayer与UIBezierPath配合画圆角