图像显示原理
图像显示的大概流程:
- 程序运行从内存中读取数据
- 对图片进行解压得到像素数据,若GPU不支持图片的颜色格式,CPU需要进行格式转换
- CoreText和CoreGraphics跟进文本内容生成位图
- 然后解压后的数据或位图通过GPU Bus上传到GPU,GPU需要将每一个frame的纹理(位图)合成在一起(一秒60次)。每一个纹理会占用VRAM(video RAM),所以需要给 GPU 同时保持纹理的数量做一个限制。GPU 在合成方面非常高效,但是某些合成任务却比其他更复杂,并且 GPU在 16.7ms(1/60s)内能做的工作也是有限的。
CPU的工作
- 对象的创建、调整和销毁
- 布局计算
- 文本计算
- 文本渲染
- 图片的解码
- 图像的绘制
GPU的工作
- 纹理的渲染
- 视图合成
- 图形生成
GPU显示图像
- CPU 计算好显示内容提交到 GPU
- GPU 渲染完成后将渲染结果放入帧缓冲区
- 随后视频控制器会按照VSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。
垂直同步机制
当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync)
首先从过去的CRT显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
当开启垂直同步后,GPU 会等待显示器的VSync信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。
卡顿的产生
在VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,App主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。
离屏渲染
GPU的渲染方式
-
On-Screen Rendering(当前屏幕渲染):指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
-
Off-Screen Rendering (离屏渲染),指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
OffScreen Rendering 则多了一个步骤,GPU 会先创建一个屏外缓冲区(OffScreenBuffer),然后在其中进行渲染,最后将渲染结果提交到帧缓冲区内(FrameBuffer);这其中还涉及到了两次上下文的转换,首先把当前上下文转换到屏外缓冲区(OffScreenBuffer),然后又转换到帧缓冲区(FrameBuffer)。整个过程会造成很大的消耗。例如蒙板操作:
在前两个渲染通道中,GPU分别得到了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把两者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,因此相对于普通渲染更占用资源。
CPU 渲染
如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。由CPU处理的一种特殊渲染方式,在App内同步完成,渲染得到的bitmap最后再交由GPU用于显示,由于CPU自身做渲染的性能也不好,所以这种方式也是需要尽量避免的。
为何需要离屏渲染
一些复杂的效果,如:圆角,阴影,遮罩,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,无法直接渲染出结果,所以就需要屏幕外渲染被唤起。
屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。
所以当使用离屏渲染的时候会很容易造成性能消耗,因为在OPENGL里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。
触发离屏渲染的操作
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- 复杂形状设置圆角等
光栅化:
概念:将图转化为一个个栅格组成的图象。
特点:每个元素对应帧缓冲区中的一像素。
shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。
“Color Hits Green and Misses Red”可以检查当前场景下光栅化操作,绿色表示缓存被复用,红色表示缓存在被重复创建。
如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。
注意:
对于经常变动的内容,不要开启光栅化,则会造成大量的离屏渲染,降低图形性能。
圆角的设置
-
使用cornerRadius
self.layer.cornerRadius = cornerRadius; self.layer.masksToBounds = YES; //防止子view边界超过父view //self.clipsToBounds = YES; self.layer.shouldRasterize = YES; //光栅化
- UIView的clipsToBounds与CALayer的maskToBounds的作用一致,防止子view边界超过父view
- cornerRadius默认情况下只对背景色和border起作用
- 如果最后设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为 contentsScale
-
使用贝塞尔曲线 + maskLayer
- (void)setRoundRect:(CGRect)frame cornerRadius:(CGFloat)cornerRadius { CGRect maskFrame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame)); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:maskFrame cornerRadius:cornerRadius]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; maskLayer.frame = maskFrame; maskLayer.path = path.CGPath; self.layer.mask = maskLayer; }
UIBezierPath对CoreGraphics进行了一层封装
-
使用CoreGraphics绘制圆角图片做背景(CPU渲染)
- (void)drawRoundCornerWithCornerRadius:(CGFloat)cornerRadius { CGFloat width = CGRectGetWidth(self.frame); CGFloat height = CGRectGetHeight(self.frame); UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, width, height)]; [self addSubview:imageView]; dispatch_async(dispatch_queue_create("backgroundQueue", DISPATCH_QUEUE_CONCURRENT), ^{ UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, [UIScreen mainScreen].scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextMoveToPoint(context, 0, 0); CGContextAddArcToPoint(context, width, 0, width, height, cornerRadius); CGContextAddArcToPoint(context, width, height, 0, height, cornerRadius); CGContextAddArcToPoint(context, 0, height, 0, 0, cornerRadius); CGContextAddArcToPoint(context, 0, 0, width, 0, cornerRadius); CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor); CGContextDrawPath(context, kCGPathStroke); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); dispatch_async(dispatch_get_main_queue(), ^{ imageView.image = image; }); }); }
CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程
-
将图片剪切为圆角(针对图片)
- (UIImage *)setRoundCornerRadius:(CGFloat)cornerRadius { UIImage *image = nil; CGRect imageFrame = CGRectMake(0, 0, self.size.width, self.size.height); UIGraphicsBeginImageContextWithOptions(self.size, NO, [UIScreen mainScreen].scale); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius]; [path addClip]; [self drawInRect:imageFrame]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
使用圆角图片作为蒙板
如何避免离屏渲染
- 圆角视图较少,使用使用cornerRadius
- UIImageView 的圆角通过直接截取图片实现,其它视图的圆角可以通过 Core Graphics 画出圆角矩形实现。
- 对于图形采用异步绘制
- 直接使用圆角素材作为背景
参考资料
iOS离屏渲染优化
绘制像素到屏幕上
关于性能的一些问题(iOS)
解决常见的masksToBounds离屏渲染带来的性能损耗
iOS 离屏渲染的研究
小心别让圆角成了你列表的帧数杀手
iOS 高效添加圆角效果实战讲解
UIKit性能调优实战讲解
iOS 保持界面流畅的技巧
iOS开发:关于图形渲染以及界面优化的的一些想法
iOS图形渲染分析