【iOS】离屏渲染

【iOS】离屏渲染
前言这里我简单学习一下有关iOS渲染的相关知识。屏幕卡顿在我们日常使用过程中,经常会遇到屏幕卡顿的问题。(图形撕裂、掉帧等问题)产生原因CPU和GPU在渲染的流水线中耗时过长,导致缓存区获取位图然后显示的时候,下一帧的数据还没有准备好从而产生了掉帧的现象苹果针对屏幕撕裂的为题采用的是垂直同步+双缓存机制,可以有效防止屏幕撕裂,但是同时容易产生掉帧于是在垂直同步+爽滑黁区的方案上优化为三缓存区,但是还是没办法从根本上解决掉帧问题,知识相比双缓存区掉帧的概率更小。屏幕撕裂屏幕成像过程首先GPU将需要显示的图像进行渲染然后将渲染后的结果以位图的形式存储到帧缓存区视频控制器从帧缓存区读取位图数据之后交给显示器,从左上角扫描显示这个流程中,很容易出现一个屏幕撕裂的问题,如下:在屏幕显示图形图像的过程中,是不断的从帧缓存区获取一帧一帧的数据进行显示的,在渲染过程中,如果屏幕拿到的数据是旧的,就会显示旧的数据。在旧的数据没有读取完成时,新的一帧的数据处理好了放到缓存区之后,又会导致屏幕另一部分的显示是获取的新数据,导致屏幕上呈现图片不匹配、任务景象显示错位等情况。苹果采用垂直同步+双缓存区的机制,强制要求同步,但是会出现掉帧问题:垂直同步:给帧缓存区加锁,当电子光束扫描的过程中,只有扫描完成之后才会继续读取下一帧的数据。双缓存区:采用两个缓存区交替当作GPU结果的存储区,当屏幕显示其中一个缓存区的时候,另一个缓存区继续等待下一个渲染结果,两个缓存区交替进行。掉帧在VSync信号到来之前,系统图形服务会通过CADisplayLink等机制通知App,主线程开始在CPU种计算显示内容,随后GPU进行变换、合成、渲染,完成之后会将渲染结果提交到帧缓存区中去,等待下一次同步信号到来。如果在下一个同步信号到来之前,CPU或者GPU没有处理完内容并提交,那一帧就会丢弃,等待下一次机会显示。产生掉帧。CPU/GPU 必须在下一次 VSync 到来之前完成一帧的绘制。若未完成,显示器无法切换到新缓冲区,只能重复显示上一帧,于是产生掉帧和卡顿。卡顿监控:主流的卡顿监控主要有下面两种:FPS监控:为了保持流畅的UI交互,App的刷新频率应该在60fps左右(iOS设备默认的刷新率)。而一次刷新的事件间隔16.67ms,如果这个时间内数据没有准备好就会产生卡顿。主线程卡顿监控:通过自线程检测主线程runloop。这个方法笔者前面在学习runloop的时候有给过具体的实现流程,这里不在赘述。主要就是通过KCFRunLoopBeforeSources和KCFRunLoopAfterWaiting之间的耗时判断。FPS监控参照YYKit中的YYFPSLabel,主要通过CADisplaylink实现,借助link时间差,计算一次刷新所需要的时间,然后通过刷新次数/时间差判断范围,通过不同颜色文字表示卡顿程度。本质上就是一个悬浮在窗口上的FPS实时监控器。这里测量的不是GPU最终真是输出的帧数,而是主线程实际处理了多少次CADisplaylink回调,判断主线程是否阻塞RunLoop是否卡顿、界面滚动是否流畅。#import"ZYLFPSLabel.h"#importQuartzCore/QuartzCore.h/// FPS 标签在 window 上的唯一 tag,用于 show 时去重。staticNSIntegerconstkZYLFPSLabelTag=2026071401;/// FPS 文案刷新间隔。////// CADisplayLink 每一帧都会回调,如果每帧都改 UI,数字会抖动得很厉害,/// 也会让监控本身引入不必要的额外开销。0.25 秒是一个比较灵敏、同时又稳定的窗口。staticCFTimeIntervalconstkZYLFPSUpdateInterval=0.25;@interfaceZYLFPSLabel()/// CADisplayLink 会在屏幕每次准备刷新时向 target 发送 selector。/// 它的 timestamp 是当前这次回调对应的时间点,适合用来计算一段时间内实际刷新了多少次。@property(nonatomic,strong)CADisplayLink*displayLink;/// 统计窗口内 CADisplayLink 回调的次数。/// 理想 60Hz 屏幕上,1 秒大约回调 60 次;120Hz 屏幕上,1 秒大约回调 120 次。@property(nonatomic,assign)NSUInteger frameCount;/// 上一次刷新 FPS 文案的时间点。/// 当 timestamp - lastTime 达到 kZYLFPSUpdateInterval 时,计算并展示一次 FPS。@property(nonatomic,assign)CFTimeInterval lastTime;@end@implementationZYLFPSLabel+(instancetype)sharedFPSLabel{staticZYLFPSLabel*label=nil;staticdispatch_once_t onceToken;dispatch_once(onceToken,^{label=[[ZYLFPSLabel alloc]initWithFrame:CGRectMake(12,88,78,28)];});returnlabel;}-(instancetype)initWithFrame:(CGRect