iOS

UI渲染

Posted by Maqy on July 8, 2018

关于UI渲染,其实我了解的也只是个大概,所以在这里简单记一下,主要为了解惑下列问题。

1.UI渲染流程是怎么样的,从何而起,又从何而终?

2.渲染过程中需要注意些什么?

3.什么是离屏渲染?

4.怎么异步渲染?

5.怎么对渲染性能调优?

前言

其实各个平台系统都是有渲染的,而且基本都是基于OpenGL来做的渲染,而基本流程都是CPU计算,GPU渲染,其渲染流程是基本固定的,而OpenGL里把从CPU到GPU到最后呈现在屏幕上的过程称为管道,这个流程里部分操作步骤我们是可以干预的,部分不可干预,下面就简单说下个部分的工作内容吧~

CPU & GPU

CPU又叫中央处理器,用于计算处理任务,复杂的计算。

GPU又叫图形处理器,是一种微型的多核的专用微处理器。

两者只不过是计算能力不同,CPU适用于处理大量计算,所以被用来处理程序中各种乱七八糟的复杂逻辑,而GPU只能进行一些微量计算,所以GPU用于渲染图层,而CPU用于计算空间坐标系等。

垂直同步信号

由显示器发出的一个垂直同步信号(vertical synchronization),简称 VSync,其频率就是我们通常知道每秒60帧,CADisplayLink就是和这个同步的。该信号是根据硬件的时间来的。

显示器

CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示. 其流程如下:

CPU --> GPU --> Frame Buffer(帧缓冲区) --> Video Controller(视频控制器) --> monitor

视频控制器输出到屏幕上的时候是逐行打印的。

OpenGL图像管道

这个处理过程是包括CPU和GPU的,其详细的过程我也不太了解,只是当时听分享的时候听明白了,后来时间长就忘了,只剩了一些大概的印象,所以赶紧记下来。

第一个需要同步的知识点是OpenGL对图像的处理会把任何图像分割成基本图元,而图元只有三种类型:点,线,三角形。

第二个点是我们的屏幕实际上在渲染的时候都是切割成一些简单的三角形的,以三角形为单位进行绘制的,特别小的三角形。

第三点是图形管道的处理过程如下:

上图中蓝色的代表可编译的部分,而绿色的都是一些固定的程序,是有CPU或GPU提供的固定的处理方法来进行处理的。

Vertex shader: 顶点着色器
Tessellation control shader && Tessellation evaluation shader: 细分曲面着色器
Geometry shader: 几何着色器,根据坐标进行图元的裁剪,可以减少图元(比如两个坐标点,一个在前一个在后,如果前面的完全不透明,就可以把后面的完全丢弃了,在三维动画中很常见)
Fragment shader: 片段着色器,用于计算你最后的颜色输出(比如你一个像素需要显示两个点,但颜色不一样还带透明度,就是在这里混合的)

其实着色器可以认为是这个小程序的简称,用于标记它的作用。
而通过上面的Geometry shader我们可以知道为什么苹果鼓励我们尽量将view的颜色设置成不透明的了,因为可以在这一步做一些丢弃,不用绘制的就不绘制了,这样也就么有下面的图层混合了。

第四点任何OpenGL里使用的坐标系都是三维坐标系,不管你程序里用的是二维坐标系还是三维。

离屏渲染 & CPU渲染

其实CPU渲染很简单,就是用CPU创建绘制图,举个栗子:

CGContextRef cx = UIGraphicsGetCurrentContext();
CGContextBeginPath(cx);
CGContextSetLineWidth(cx, 10);
CGContextSetStrokeColorWithColor(cx, UIColor.blueColor.CGColor);

CGFloat dash[] = {10,10};
CGContextSetLineDash(cx, 0, dash, 2);
CGFloat startPointX = 0;
CGFloat startPointY = 0;
CGContextMoveToPoint(cx, startPointX, startPointY);
CGFloat endPointX = rect.size.width;
CGFloat endPointY = rect.size.height;
CGContextAddLineToPoint(cx, endPointX, endPointY);
CGContextStrokePath(cx);
CGContextClosePath(cx);

而离屏渲染就比较复杂了,到现在我也没有很好的理解什么是离屏渲染。我看网上有比较靠谱的说法,然后结合自己的理解大致如下吧:

所谓离屏渲染为什么会触发呢?其原因可能是 iOS 里所有的view都是四四方方的,当我们设置一些圆角的时候,其实iOS内部并不知道圆角这部分的准确坐标,所以需要通过图层混合的方法,一层一层的进行绘制,也就是因为在几何着色器那里并不能计算出覆盖的坐标,导致片段着色器已经渲染上,之后要修改就需要通过覆盖的方式来进行。而因为这种图层渲染我们并不能准确的预估工作量的大小,所以就开辟了一个单独的缓冲区来存储这块内容。

而离屏渲染主要的操作耗时是因为我们在绘制的时候需要切换上下文,而且我们还需要开辟用于存储离屏渲染的缓冲区,这就是瓶颈所在。


举个栗子:

@interface CPUDraw : UIView
@end
@implementation CPUDraw
- (void)drawRect:(CGRect)rect {
    CGContextRef cx = UIGraphicsGetCurrentContext();
    CGContextBeginPath(cx);
    CGContextSetLineWidth(cx, 10);
    CGContextSetStrokeColorWithColor(cx, UIColor.blueColor.CGColor);

    CGFloat dash[] = {10,10};
    CGContextSetLineDash(cx, 0, dash, 2);
    CGFloat startPointX = 0;
    CGFloat startPointY = 0;
    CGContextMoveToPoint(cx, startPointX, startPointY);
    CGFloat endPointX = rect.size.width;
    CGFloat endPointY = rect.size.height;
    CGContextAddLineToPoint(cx, endPointX, endPointY);
    CGContextStrokePath(cx);
    CGContextClosePath(cx);
}
@end

- (instancetype)initWithView:(UIView *)view {
    if (self = [super init]) {
    // 看看圆角是否有离屏渲染
        self.draw = [UIView new];
        self.draw.layer.cornerRadius = 60;
        [view addSubview:self.draw];
        
        // 看看label是否有离屏渲染
        self.label = [UILabel new];
        [view addSubview:self.label];
        self.label.text = @"112321312312321321";
        
        // 看看CPU渲染
        self.cpu = [CPUDraw new];
        [view addSubview:self.cpu];
    }
    return self;
}

通过上面的例子可以看到在 Colors blended Layers 查看是否有离屏渲染的时候,可以看见圆角和label是离屏渲染,而CPU渲染不是离屏渲染,可以看到 CPU 渲染 和 离屏渲染不是一个东西。

异步渲染

其实异步渲染就是利用 CPU渲染 在子线程去绘制图层,然后直接设置回主线程,举个栗子:

dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
//        CGContextRef ctx = CGBitmapContextCreate(NULL, self.size.width, self.size.height,
//        CGImageGetBitsPerComponent(self.CGImage), 0,
//        CGImageGetColorSpace(self.CGImage),
//        CGImageGetBitmapInfo(self.CGImage));
//        CGImageRef img = CGBitmapContextCreateImage(ctx);
//        CFRelease(ctx);
        dispatch_async(dispatch_get_main_queue(), ^{
            self.layer.contents = img;
        });
    });

总结

这里就简单说下性能调优的吧,我们通过Colors blended Layers可以看到视图上个部分的性能消耗,颜色越红的部分性能越低,需要我们进行优化,常用的方法如下:

1.尽量设置不透明

2.如果用圆角可以用UIBezearPath

3.如果肯定触发离屏渲染了,就把光栅化设置成YES吧,这样有缓存

4.shadow 和 mask 之类都会触发离屏渲染

参考

1.http://hchong.net/2019/05/11/iOS%E5%BC%80%E5%8F%91UI-UI%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86/

2.https://zhuanlan.zhihu.com/p/72653360