MT iOS

iOS 渲染原理总结

2020-06-01  本文已影响0人  NapoleonY

1. 图像渲染流程


如图所示,CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器按照垂直同步信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示

2. 什么是离屏渲染


正常的渲染流程如图所示,App 通过 CPU 与 GPU 合作,不停的将内容渲染完成,放入到 FrameBuffer 中,显示屏不断的从 FrameBuffer 中读取数据,显示到屏幕上



而离屏渲染则是先创建离屏渲染缓冲区 OffscreenBuffer,将渲染好的内容放入其中,等到合适的时机再将 OffscreenBuffer 中的内容进一步叠加、渲染,完成后才将内容放入 FrameBuffer中。

3. 为什么要减少离屏渲染

  1. 离屏渲染需要 App 对内容进行额外的渲染,并保存到 OffscreenBuffer ,
  2. 需要对 OffscreenBuffer 和 FrameBuffer 中的内容进行切换(Buffer 切换的代价比较大)。
  3. OffscreenBuffer 本身需要额外的空间,大量的离屏渲染可能早晨过大的内存压力。

4. 离屏渲染的具体过程是怎样的


图层的叠加绘制,大体遵循“画家算法”。如图所示,在这种算法下会按层绘制,先绘制距离较远的场景,然后绘制距离较近的场景,覆盖较远的部分。因此在普通的layer绘制中,上层的 subLayer 会覆盖下层 subLayer,下层 subLayer 绘制完成后就可以抛弃了,从而节约空间。所有的 subLayer 绘制完成后,整个绘制就完成了,就放入 frameBuffer 准备呈现到显示器上。

假设要绘制一个三层 layer,不设置 cornerRadius 和 maskToBounds,整个过程如图所示
而当设置了 cornerRadius 和 maskToBounds = true 时,maskToBounds 会应用到所有的 subLayer 上。这也意味着所有的 subLayer 必须要重新应用一次圆角+裁切,因此所有的 subLayer 在第一次绘制结束后不能被丢弃,而必须保存在 OffscreenBuffer 中等待下一轮圆角+裁切,因此诱发了离屏渲染。

5. iOS 渲染具体过程


app 处理流程如下
app 本身不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。App 将渲染任务和数据提交给 Render Server,Render Server 处理完数据后再传递给 GPU,最后由 GPU 调用 iOS 的图像设备进行显示。具体流程如下:
1. app 处理事件如用户的点击,在此过程中 app 可能需要更新时图树,相应的涂层树也会更新。
2. app 通过 CPU 完成对显示内容的计算,如视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算后,app 对图层进行打包,并将其发送至 Render Server,即完成了一次 Commit Transaction 操作。
3. Render Server 执行 OpenGL、CoreGraphics 相关程序,并调用 GPU
4. GPU 则在物理层上完成对图像的渲染。
5. GPU 通过 frameBuffer 、视频控制器等相关部件,将图像显示在屏幕上。
上述步骤远超过了 16.67ms,因此为了满足对屏幕 60FPS 的刷新率的支持,需要将这些步骤分解,以流水线的形式,并行执行,如下图所示。

其中,app 调用 Render Server 前的最后一步 Commit Transaction 可以分为4个步骤:Layout、Display、Prepare、Commit

6. GPU 渲染流程反映到具体的 iOS 代码上都有哪些步骤

7. 为何要图片解码,直接显示图片有什么问题?

图片格式
  1. 位图:位图是一个像素数组,数组中的每个像素就代表着图片中的一个点
  2. JPEG、PNG: 一种压缩的位图图形格式。其中 PNG 图片是无损压缩,并且支持 alpha 通道,JPED 图片是有损压缩,可以指定 0~100% 的压缩比
解码

通过网络下载的图片或者本地的图片,都是 JPEG、PNG这些格式的压缩图片。在将这些图片渲染到屏幕之前,首先要得到图片的原始像素数据,才能执行后续的绘制操作
iOS 默认在主线程对图像进行解码,解压缩后的图片大小与原始文件大小无关,只与图片像素有关
解压缩后的图片大小 = 图片的像素宽 * 图片的像素高 * 每个像素所占的字节数

8. render server 具体是啥,负责什么工作,为何需要单独的 RenderServer 进程

iOS Render Server 是 OpenGL ES & Core Graphics。Render Server 将与 GPU 通信把数据经过处理之后传递给 GPU。主要为

  1. 负责解析图层树,反序列化为渲染树:使用这个树状结构,渲染服务队动画的每一帧做出如下工作:对所有的图层属性计算中间值,设置 OpenGL 几何形状(纹理化的三角形)来执行渲染。
  2. 调用绘制指令,并提交到 GPU
    这两个阶段在动画过程中不停的重复。阶段 1 在软件层面通过CPU 处理,阶段 2 被 GPU 执行。
图层树

屏幕上的可视内容倍分解成为独立的图层(CALayer),这些图层被存储在一个叫做图层树的体系中。

App 并不是完全占有 Screen,还有状态栏、通知栏、上拉菜单等,需要一个统一的 Server 来负责

9. layer 为啥可以显示内容?layer 的backing store 与 frameBuffer(帧缓存或显存、显示存储器) 都是啥,里面存的都是位图?关系如何?

CALayer 包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图。iOS 中将该缓存区保存的图片称为寄宿图(CGImageRef)
GPU 包含着一部分显存空间(VRAM),在设备启动的时候,作为 PCI 设备的 GPU,其显存空间中的一部分地址,会被映射到 PCI 地址中,然后再把 PCI 总线上的地址映射到 CPU 地址中。这样 CPU 就能通过对这段映射后的地址的访问,访问到 GPU 的存储空间。此外还可以通过内存空间进行数据交互。首先系统为 GPU 动态分配一些不连续的空间(GTT),用于映射到 GPU 显存空间中。然后 CPU 通过对这段空间的访问,进行对 GPU 显存空间的访问。对于 GPU 指令来说,则是直接由 CPU 通过 PCI 总线推送到 GPU 中,或是由 GPU 自己从指令流中获取指令。
layer 的backing store和 frameBuffer 存储的都是位图。frameBuffer 中的位图是最终显示到屏幕上的。

image.png
以显示图片为例。图片数据是以纹理形式,进入 OpenGL 渲染流程的,就像上面的流程图。
参照问题5(iOS 渲染具体过程)layer 中的位图数据会与其他的位图(如果还有其他 layer的话)经过片段着色器、测试与混合阶段,变成新的位图数据,输出到 frameBuffer 显示到屏幕上

思考

  1. 想显示渐变带边框带阴影的 View 有几种方式?各有什么优缺点?
  2. 在 TableViewCell 中显示圆形头像有几种方式?各有什么优缺点?
    1. maskToBounds 与 cornerRadius 组合:离屏渲染
    2. 上方叠加一个中间圆形镂空的图片:多了一层 ImageView
    3. UIBezierPath 设置的 CAShapeLayer 作为 UIImageView.layer.mask:离屏渲染
    4. CoreGraphics 设置处理 UIImage ,注意是直接将 UIImage 绘制成圆形,而不是调用 UIImageView drawRect::推荐
func ny_image(byRoundCornerRadius radius: CGFloat,
                                     corners: UIRectCorner,
                                 borderWidth: CGFloat,
                                 borderColor: UIColor,
                              borderLineJoin: CGLineJoin) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        let context = UIGraphicsGetCurrentContext()
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        // 根据坐标系旋转图片
        context?.scaleBy(x: 1, y: -1)
        context?.translateBy(x: 0, y: -rect.size.height)

        // 剪成圆形
        let minSize = min(size.width, size.height)
        if borderWidth < minSize / 2 {
            let path = UIBezierPath.init(roundedRect: rect.insetBy(dx: borderWidth, dy: borderWidth), byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
            path.close()
            context?.saveGState()
            path.addClip()
            context?.draw(cgImage!, in: rect)
            context?.restoreGState()
        }

        // 画边框
        if (borderColor != .clear) && (borderWidth < minSize / 2) && (borderWidth > 0) {
            let strokeInset = (floor(borderWidth * scale) + 0.5) / scale
            let strokeRect = rect.insetBy(dx: strokeInset, dy: strokeInset)
            let strokeRadius = radius > scale / 2 ? radius - scale / 2 : 0
            let path = UIBezierPath.init(roundedRect: strokeRect, byRoundingCorners: corners, cornerRadii: CGSize(width: strokeRadius, height: strokeRadius))
            path.close()

            path.lineWidth = borderWidth
            path.lineJoinStyle = borderLineJoin
            borderColor.setStroke()
            path.stroke()
        }

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }

参考

  1. iOS 保持界面流畅的技巧
  2. 绘制像素到屏幕上
  3. iOS 渲染原理解析
  4. iOS 图像渲染原理
  5. 图像显示、OpenGL、离屏渲染、滤镜等等的一些小事
上一篇下一篇

猜你喜欢

热点阅读