# 11.离屏渲染

## 一: 什么是离屏渲染

> 如果要爱屏幕上显示内容,我们需要一块至少和屏幕数据量一样大的`FrameBuffer`作为像素数据的缓存区域,也是GPU储存渲染结果的地方. 如果有时面临一些限制,无法把渲染结果直接写入`FrameBuffer`而是存在别的区域之后再写入`FrameBuffer`这个过程就是`离屏渲染`

App -> OffscreenBuffer -> FrameBuffer

## 二: CPU离屏渲染

* UIView中实现了`drawRect`就算内部没有实际代码,系统也会为这个view申请一片内存区域等待`CoreGraphics`可能的绘画操作
* 很多人对于开一块`CGContext`来画图的操作也成为离屏渲染,因为数据是存入了CGContext而不是FrameBuffer
* 进一步讲其实所有CPU进行的光栅化操作(文字渲染,图片解码)都无法直接绘制到有GPU管理的`FrameBuffer`中,只能暂时放在另一块内存中
* 但是苹果工程师说CPU渲染并非真正意义的离屏渲染
  * <https://lobste.rs/s/ckm4uw/performance\\_minded\\_take\\_on\\_ios\\_design#c\\_itdkfh>
  * 实现DrawRect打开Xcode调试的“Color offscreen rendered yellow”开关,该区域不会变黄

## 三: GPU渲染

### 画家算法

> 主要渲染操作都是由`CoreAnimation`和`Render Server`模块通过调用显卡驱动提供的接口OpenGL/Metal来执行的

* 对于每一层Layer,`Render Server`会按照深度排序依次输出到FrameBuffer
* 后一层覆盖前一层,达到最终效果
* 此过程不可逆
  * 遮住的部分数据永久丢失无法修改
* 想要突破这个限制
  * 在FrameBuffer外开辟一块空间
  * 把待处理的Layer画上去
  * 进行修改后再写回FrameBuffer

### GPU离屏渲染

* 上面开辟空间渲染,真个过程就算离屏渲染
* 对于每层Layer如果能通过单次遍历即可完成渲染的话效率最高
  * 否则只能开辟离屏Buffer来完成一些复杂操作

## 四: 常见离屏渲染场景

### 4.1 cornerRadius+clipsToBounds

* 如果只是设置cornerRadius,而不剪切内容,或者只裁掉矩形区域以外的内容,并不会触发离屏渲染

### 4.2 shadow

* 阴影的形状未必是矩形
* 只能在Layer以上的内容全部渲染完成后才能知道形状是什么
  * 这样只能离屏渲染,将上层内容渲染完成,知道了shadow的形状,将shadow添加到FrameBuffer
  * 再把内容回执上去
  * 如果事先告诉了CoreAnimation(通过shadowPath)阴影的几何形状的话就不需要依赖Layer了

### 4.3 group opacity

* 两个带透明度的图层叠加
* 叠加部分无法通过一次遍历即完成
* 会触发离屏渲染

### 4.4 mask

* 作用在Layer所有子Layer之上的而且可能有透明度
* 离屏渲染

### 4.5 UIBlurEffect

* 无法一次遍历完成

### 4.6 allowsEdgeAntialiasing 抗锯齿

* 无法一次遍历完成

### 4.7 UIButton & UIImageView

* `UIButton`
  * 默认自带背景图层
  * 切圆角会有离屏渲染

```C++
// clipsToBounds = YES 有
- (UIButton *)bgImageButton {
    if (!_imageButton) {
        _imageButton = [[UIButton alloc] init];
        [_imageButton setBackgroundImage:[UIImage imageNamed:@"bg_image"] forState:UIControlStateNormal];
        _imageButton.layer.cornerRadius = 15;
        _imageButton.contentMode = UIViewContentModeScaleAspectFill;
        _imageButton.clipsToBounds = YES;
    }
    return _imageButton;
}

// clipsToBounds = YES 有
- (UIButton *)imageButton {
    if (!_bgImageButton) {
        _bgImageButton = [[UIButton alloc] init];
        [_bgImageButton setImage:[UIImage imageNamed:@"bg_image"] forState:UIControlStateNormal];
        _bgImageButton.layer.cornerRadius = 15;
        _bgImageButton.contentMode = UIViewContentModeScaleAspectFill;
        _bgImageButton.clipsToBounds = YES;
    }
    return _bgImageButton;
}

- (UIButton *)colorButton {
    if (!_colorButton) {
        _colorButton = [[UIButton alloc] init];
        [_colorButton setBackgroundColor:[UIColor greenColor]];
        _colorButton.layer.cornerRadius = 15;
        _colorButton.clipsToBounds = YES;
        // 不加 title 就没有
        [_colorButton setTitle:@"Ryukie" forState:UIControlStateNormal];
    }
    return _colorButton;
}

```

* `UIImageView`
  * 默认无背景图层
  * 默认情况下直接图片切圆角不会触发离屏渲染
  * 如果内部加了子视图会离屏渲染

```C++
// 没有
- (UIImageView *)imageView {
    if (!_imageView) {
        _imageView = [[UIImageView alloc] init];
        _imageView.image = [UIImage imageNamed:@"bg_image"];
        _imageView.layer.cornerRadius = 15;
        _imageView.clipsToBounds = YES;
    }
    return _imageView;
}

// 有
- (UIImageView *)imageView {
    if (!_imageView) {
        _imageView = [[UIImageView alloc] init];
        _imageView.image = [UIImage imageNamed:@"bg_image"];
        _imageView.layer.cornerRadius = 15;
        _imageView.clipsToBounds = YES;
        
        UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
        redView.backgroundColor = [UIColor redColor];
        [_imageView addSubview:redView];
    }
    return _imageView;
}
```

## 五: GPU离屏渲染性能影响

* GPU操作是高度流水线化的.正常情况一直在向FrameBuffer中输出
* 当需要开启另一块内存进行其他操作,要暂时停止向FrameBuffer中输出,进行相关操作
* 完成后再向FrameBuffer中输出内容

> TableView/CollectionView中滚动每一帧变化都会触发重绘 一旦发生离屏渲染,上面的切换操作将会发生很多次(60/s)

## 六: 利用离屏渲染

### 6.1 只离屏渲染一次

* CALayer的shouldRasterize属性
  * 可以把Layer及其子Layer,还有圆角等内容保存在内存中
  * 下一帧可以服用,而不会继续触发离屏渲染
  * 注意
    * 如果本来就不复杂,也没有圆角等,打开反而会触发一次不必要的离屏渲染
    * 缓存有限,不会超过屏幕总像素的2.5倍
    * 超过100ms没用就被释放
    * 静态内容,如果有resize或动画的话就失效了,需要重新离屏渲染
      * 针对这种情况，Xcode提供了“Color Hits Green and Misses Red”的选项，帮助我们查看缓存的使用是否符合预期
  * 除了用来优化离屏渲染还可以用于展示十分复杂的Layer

## 七: 什么时候需要CPU渲染

* 渲染新能调优核心在于
  * 平衡CPU\&GPU的负载
  * 让他们各自做自己擅长的事情
* 文字(CoreText使用CoreGraphics渲染)图片(ImageIO)的渲染
  * GPU不擅长这些事情
  * 需要CPU处理完再交给GPU
    * 例如:
    * 一个圆角实现
      * 用CoreGraphics给图片加圆角-CPU完成
      * 这样就不用给容器设置cornerRadius避免可能发生的离屏渲染
* 注意点:
  * 渲染不是CPU强项,调用CoreGraphics会消耗一定的计算时间,因此CPU渲染放在后台线程去完成,然后回到主线程将结果返回给CoreAnimation.但是多线程间数据同步会增加一定的复杂度
    * 也是AsyncDisplayKit/Texture的主要思想
  * CPU渲染速度慢,适合不复杂的元素
    * 没有硬件加速的视频解码...酸爽啊
  * 渲染结果bitmap数据量较大(一般为解码后的UIImage),需要及时释放
  * 选择用CPU渲染的话就没必要触发GPU的离屏渲染,否则会有两块相同内存
  * 善用工具进行检测

## 八: 优化

* 使用AsyncDisplayKit/Texture框架
* 图片圆角
  * 采用不适用容器裁剪的方式,用上面提到过的预处理图片的方式
* 视屏圆角
  * 创建四个弧形Layer盖住四个角
  * 图片也可以
* View的圆形边框
  * 如果没有backgroundColor可以使用cornerRadius来做
* 阴影
  * 使用shadowPath
* 特殊形状View
  * 使用layer mask并打开shouldRasterize来对渲染结果进行缓存
* 模糊
  * 不采用系统提供的UIVisualEffect，而是另外实现模糊效果（CIGaussianBlur），并手动管理渲染结果

## 九: 总结

> CPU渲染虽然也是“离屏”，但是通常提到的离屏渲染是发生在GPU 如果一个layer无法在一次遍历就完成绘制，那么就不得不触发离屏渲染 离屏渲染的开销主要在与frame buffer与离屏buffer之间的上下文切换。如果无法避免，也可以通过有效利用shouldRasterize，减少触发的次数 CPU和GPU是相互扶持的关系。CPU渲染效率不高，但是较为通用灵活；GPU擅长并行计算，但也有捉襟见肘之时，此时CPU可以适当给与帮助

## 参考

> <https://medium.com/@jasonyuh/关于离屏渲染的深入研究-e776f56b3e60> <https://blog.ibireme.com/2015/11/12/smooth\\_user\\_interfaces\\_for\\_ios/>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryukiedev.gitbook.io/wiki/ios/you-hua/11.-li-ping-xuan-ran.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
