一.图像显示的基本流程
GPU是一种高性能的并发计算单元,GPU驱动是一些直接操作GPU的代码,由于各个GPU是不同的,在驱动之上创建一个通用层,通常是OpenGL/OpenGL ES。由于OpenGL是对GPU操作的基本通用封装,对于应用层开发者来说实在是太底层了,所以Apple分别封装了CoreGraphics
,CoreAnimation
,CoreImage
来辅助开发。
通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号,逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。
随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。
由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,这就是界面卡顿的原因。
具体过程
在iOS APP中渲染一个视图也要经过上述的逻辑,以动画为例:
- CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等。
- RenderServer解析提交的子树状态,生成绘制指令。
- GPU执行绘制指令。
- 显示渲染后的数据。
App在应用层主要通过提交会话和渲染指示这两个过程来控制整个渲染的主要逻辑。
提交会话
- 布局(Layout):调用layoutSubviews方法和调用addSubview:方法。(会造成CPU和I/O瓶颈)
- 显示(Display):通过drawRect绘制视图/绘制string(字符串)。
- 准备提交(Prepare):解码图片和图片格式转换。(GPU不支持的某些图片格式,尽量使用GPU能支持的图片格式)
- 提交(Commit):打包layers并发送到渲染server,递归提交子树的layers,如果子树太复杂,会消耗很大,对性能造成影响。
渲染指示
OpenGL中,GPU屏幕渲染有以下两种方式:
- On-Screen Rendering:意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
- Off-Screen Rendering:意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在如下两个方面:
- 创建新的缓冲区。
- 上下文切换。离屏渲染的整个过程,需要多次切换上下文环境:先从当前屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果显示到到屏幕上,这又需要将上下文环境从离屏切换到当前屏幕。
On-Screen Rendering
- CommandBuffer,接受OpenGL ES处理完毕的渲染指令。
- Tiler,调用顶点着色器,把顶点数据进行分块(Tiling)。
- ParameterBuffer,接受分块完毕的tile和对应的渲染参数。
- Renderer,调用片元着色器,进行像素渲染。
- RenderBuffer,存储渲染完毕的像素。
Off-Screen Rendering
当设置了以下属性时,会触发离屏渲染:
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
为了避免卡顿问题,应当尽可能使用当前屏幕渲染,可以不使用离屏渲染则尽量不用,应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术。必须离屏渲染时,相对简单的视图应该使用 CPU 渲染,相对复杂的视图则使用一般的离屏渲染。
使用遮罩(Mask)
使用毛玻璃(Blur)
二.离屏渲染优化
由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染。但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。对一些简单的绘制过程来说,这个过程有可能用Core Graphics
,全部用CPU来完成反而会比GPU做得更好。
一个常见的 CPU 渲染的例子是:重写drawRect
方法,并且使用任何 Core Graphics
的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地完成,渲染得到的bitmap
最后再交由GPU用于显示。
总之,具体使用 CPU 渲染还是使用 GPU 离屏渲染更多的时候需要进行性能上的具体比较才可以。
添加圆角(Mask)
通常我们有三种方式来为一个视图添加圆角:
- 设置 cornerRadius
- UIBezierPath
- Core Graphics(为 UIView 加圆角)与直接截取图片(为 UIImageView 加圆角)
其中前两种方式均会触发离屏渲染的操作。
cornerRadius
view.layer.cornerRadius = 6.0;
view.layer.masksToBounds = YES;
这种方式会触发两次离屏渲染,如果在滚动页面中这么做的话就会遇到性能问题。当然我们可以进行缓存以优化性能,如下:
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
shouldRasterize = YES 会使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存,当然要在视图内容不改变的情况下。
注意:png 图片 在 UIImageView 这样处理圆角是不会产生离屏渲染的。(ios9.0之后不会离屏渲染,ios9.0之前还是会离屏渲染)。
UIBezierPath
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
[self.image drawInRect:bounds];
}
这种方法会触发一次离屏渲染,但是这种方式会导致内存暴增,并且同样会触发离屏渲染。
Core Graphics 为 UIView/UIImageView 加圆角
// UIBezierPath
-(UIImage *)kt_drawRectWithRoundedCorner:(CGFloat)radius SizetoFit:(CGSize)sizetoFit
{
CGRect rect = CGRectMake(0, 0, sizetoFit.width, sizetoFit.height);
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen.scale);
CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)].CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
[self drawInRect:rect];
CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
UIImage * output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output;
}
// 计算坐标
- (UIImage*)kt_drawRectWithRoundedCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBackgroundColor:(UIColor*)backgroundColor andBorderColor:(UIColor*)BorderColor{
CGSize size = CGSizeMake(((self.bounds.size.width)), self.bounds.size.height);
CGFloat halfBorderWidth = (borderWidth / 2.0);
UIGraphicsBeginImageContextWithOptions(size, false, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, borderWidth);
CGContextSetStrokeColorWithColor(context, BorderColor.CGColor);
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGFloat width = size.width;
CGFloat height = size.height;
CGContextMoveToPoint(context, width - halfBorderWidth, radius + halfBorderWidth); // 开始坐标右边开始
CGContextAddArcToPoint(context, width - halfBorderWidth, height - halfBorderWidth, width - radius - halfBorderWidth, height - halfBorderWidth, radius); // 右下角角度
CGContextAddArcToPoint(context, halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - radius - halfBorderWidth, radius); // 左下角角度
CGContextAddArcToPoint(context, halfBorderWidth, halfBorderWidth, width - halfBorderWidth, halfBorderWidth, radius); // 左上角
CGContextAddArcToPoint(context, width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, radius + halfBorderWidth, radius); // 右上角
CGContextDrawPath(UIGraphicsGetCurrentContext(),kCGPathFill);
UIImage *output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output;
}
- (void)kt_addImageViewCorner:(CGFloat)radius{
UIImageView *imageView = [[UIImageView alloc]initWithImage:[self kt_drawRectWithRoundedCorner:radius borderWidth:borderWidth backgroundColor:backgroundColor borderColor:borderColor]];
[self insertSubview:imageView atIndex:0];
}
总结
- 如果能够只用
cornerRadius
解决问题,就不用优化。 - 如果必须设置
masksToBounds
,可以参考圆角视图的数量,如果数量较少(一页只有几个)也可以考虑不用优化。 UIImageView
的圆角通过直接截取图片实现,其它视图的圆角可以通过Core Graphics
画出圆角矩形实现。
离屏渲染的优化并不是必须的,需要具体权衡。