页面的渲染流程

108 阅读10分钟

UIView 与 CALayer

概括

UIView 本身是不具备图像渲染能力的,拥有一个 layer 属性用来持有一个 CALayer 实例,我们平时操作的 UIView 的绝大部分绘图属性内部其实都是操作其拥有的 layer 属性,比如 framehidden 等。

先简单概括一下 UIView 与 CALayer 各自的作用。

  • CALayer:继承自 NSObject, 负责图像渲染,属于 QuartzCore 框架;
  • UIView:继承自 UIResponder, 主要负责事件响应,属于基于 UIKit 框架;

UIView

open class UIView : UIResponder
  open var layer: CALayer { get }
  open class var layerClassAnyClass { get }
}

如上代码所示,UIView 中有一个 layer 属性还有一个 layerClass 属性,均为只读属性,其中:

  • layer 属性返回的是 UIView 所持有的主 Layer(RootLayer) 实例,我们可以通过其来设置 UIView 没有封装的一些 layer 属性;

  • layerClass 则返回 RootLayer 所使用的类,我们可以通过重写该属性,来让 UIView 使用不同的 CALayer 来显示。如:

    class MyViewUIView {
        override class var layerClassAnyClass {
            /// 使用GL来进行绘制
            return CAEAGLLayer.self
        }
    }
    

    CALayer

CALayer 视图结构类似 UIView 的子 View 树形结构,它们分别可以有自己的 SubView 和 SubLayer,可以向它的 RootLayer 上添加子 layer,来完成一些页面效果,比如说渐变等。

Layer 内部其实三份 layer tree,分别是:

  • layer tree(model tree):一般我们称模型树, 也就是各个树的节点的 model 信息, 比如常见的 frameaffineTransformbackgroundColor 等等, 这些 model 数据都是在开发中可以设置的, 我们任何对于 view/layer 的修改都能反应在 model tree 中;
  • presentation tree:这是一个中间层,我们 APP 无法主动操作, 这个层内容是 iOS 系统在 Render Server 中生成的;
  • render tree:这是直接对应于提交到 render server 上进行显示的树。

图片

CALayer Tree

CALayer 是所有 layer 的基类,其派生类会有一些特定的功能,比如绘制文本的 CATextLayer、渐变效果的 CAGradientLayer 等等。种类如下图所示。

图片

CALayer种类

我们通常见到的 layer 都是依附于一个 UIView,但是也有一些单独的 layer 不需要附加到 UIView 上,就可以直接在屏幕上显示内容,如 AVCaptureVideoPreviewLayerCAShapeLayer 等。当然附加在 UIView 上的 layer 和单独的 layer 在行为上还是会有不同的。

动画

基本上你改变一个单独的 layer 的任何属性的时候,都会触发一个从旧的值过渡到新值的简单动画,这就是所谓的隐式动画,其时长为 0.25s。然而,如果你改变的是 view 中 layer 的同一个属性,它只会从这一帧直接跳变到下一帧。尽管两种情况中都有 layer,但是当 layer 附加在 view 上时,它的默认的隐式动画的 layer 行为就不起作用了,那不显示动画的原因是什么呢?

无论何时一个可动画的 layer 属性改变时,layer 都会寻找并运行合适的 action 来实行这个改变,layer 通过向它的 delegate 发送 actionForLayer:forKey: 消息来询问提供一个对应属性变化的 actiondelegate 可以通过返回以下三者之一来进行响应:

  • 它可以返回一个动作对象,这种情况下 layer 将使用这个动作。
  • 它可以返回一个 nil,这样 layer 就会到其他地方继续寻找。
  • 它可以返回一个 NSNull 对象,告诉 layer 这里不需要执行一个动作,搜索也会就此停止。

对于依附于 UIView 的 layer 而言,view 就是这个 layer 的 delegate,并且不可改变。

属性改变时 layer 会向 view 请求一个动作,而一般情况下 view 将返回一个 NSNull,只有当属性改变发生在动画 block 中时,view 才会返回实际的动作。

这里说的 view 的 layer 是指 view 的 RootLayer,对于后添加上去的子 Layer 还是会有隐式动画的。

页面渲染流程

那么为什么 CALayer 可以呈现可视化内容呢?

因为 CALayer 基本等同于一个 纹理。纹理是 GPU 进行图像渲染的重要依据。纹理本质上就是一张图片,因此如下代码所示, CALayer 也包含一个 contents 属性,指向一块缓存区,称为 backing store,可以存放位图(bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图。而当设备屏幕进行刷新时,会从 CALayer 中读取生成的 bitmap, 进而呈现到屏幕上。

/* An object providing the contents of the layer, typically a CGImageRef,
  * but may be something else. (For example, NSImage objects are
  * supported on Mac OS X 10.6 and later.) Default value is nil.
* Animatable. */

/** Layer content properties and methods. **/
open var contents: Any?

那么绘制页面也有两种方式:

  • 一种是 手动绘制;
  • 一种是 使用图片。

使用图片

这种方式就是我们平时常见的 UIImageView 显示的形式,我们通过 CALayer 的 contents属性来配置图片。然而,contents 属性的类型为 id。在这种情况下,可以给 contents 属性赋予任何值,项目仍可以编译通过。但是在实践中,如果 content 的值不是 CGImage ,得到的图层将是空白的。

既然如此,为什么要将 contents 的属性类型定义为 id 而非 CGImage。这是因为在 Mac OS 系统中,该属性对 CGImage 和 NSImage 类型的值都起作用,而在 iOS 系统中,该属性只对 CGImage 起作用。

其实我们平时使用的 UIImage 其实是 CGImage 的一个轻量级封装, 于是很自然的, 在 UIImageView 中的 UIImage 对象直接将自己的 CGImage 图片数据作为 CALayer 的 Content 即可。但是需要注意我们传给 UIImageView 的 UIImage 中的图片可能是没有解码的,我们渲染流程中会有解码的过程。

手动绘制

先附上一份 CALayerDelegate 的定义的相关方法,后面会用到。

public protocol CALayerDelegate : NSObjectProtocol {

  optional func display(_ layerCALayer)

  @available(iOS 2.0*)
  optional func draw(_ layerCALayerin ctxCGContext)

  optional func layerWillDraw(_ layerCALayer)

  @available(iOS 2.0*)
  optional func layoutSublayers(of layerCALayer)

  optional func action(for layerCALayerforKey eventString) -> CAAction?
}

图片

CALayer渲染流程

上图是 CALayer 在渲染之前的流程,我们可以稍微进行归纳一下:

  • 当调用 [UIView setNeedsDisplay] 时,实际上会直接调用底层 layer 的同名方法 [layer setNeedsDisplay];该方法相当于在当前 layer 上打上了一个脏标记,标识其发生了变化,需要重新进行渲染,但此时它还显示原来的内容,等到下一轮 RunLoop 修改才会生效。
  • [CALayer display] 内部会先判断这个 layer 的 delegate 是否会响应 displayLayer:方法,如果不响应就会进入系统绘制流程中。如果能够响应,实际上是提供了异步绘制的入口,也就是给我们进行异步绘制留有余地。

补充一点,视图在初始化时会自动触发 setNeedsDisplay,添加到视图层级之后还会自动触发 setNeedsLayout

下面我们再分别看下上图的系统绘制流程以及异步绘制展开后相关知识。

系统绘制流程

图片

系统绘制流程

上图本质就是创建一个 backing storage 的流程,归纳一下:

  • 系统绘制时, 会先创建 backing storage(CGContextRef),我们可以理解为 CGContextRef上下文;
  • 判断 layer 是否有 delegate,然后进入到不同的渲染分支中去,但是最后无论哪两个分支, 都有 CAlayer 上传 backing store。
    • 如果有 delegate,则会执行 [layer.delegate drawLayer:inContext],然后在这个方法中会调用 view 的 drawRect: 方法,也就是我们重写 view 的 drawRect: 方法才会被调用到;
    • 如果没有 delegate,会调用 layer 的 drawInContext 方法,也就是我们可以重写的 layer 的该方法,此刻会被调用到;

注意 drawRect 方法是在 CPU 执行的, 在它执行完之后, 通过 context 将数据 (通常情况下这里的最终结果会是一个 bitmap, 类型是 CGImageRef) 写入 backing store, 通过 rendserver 交给 GPU 去渲染,将 backing  store 中的 bitmap 数据显示在屏幕上。

异步绘制

上面已经提到如果成为 layer 的 delegate,然后实现 displayLayer 方法,便可以开始异步绘制了,在异步绘制过程中:

  1. 由 delegete 去负责生成 bitmap 位图;
  2. 切换到主线程,将生成的 bitmap 作为 layer.content s 属性的值。

下图为异步绘制的时序图:

图片

异步绘制

具体的异步绘制的代码示例可查看第三方库开源库YYAsyncLayer[3]

小tips:

AutoLayout 在完成布局后,所计算出来的位置和尺寸内部修改的值是 center 和 bounds 两个属性,因此最终的展示效果不会因为仿射变换而产生异常。同时这也解释了为什么通过 AutoLayout 设置约束后修改 frame 属性来改变位置和尺寸不会起作用的原因。

涉及渲染的框架

5107384392a445bab9da99e1c18d978f~tplv-k3u1fbpfcp-jj-mark-1512-0-0-0-q75.awebp.webp UIKitUIKit 自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件响应的传递大体是经过逐层的视图树遍历实现的。其中iOS上对应的是UIKitMac OS对应的是AppKit;关于事件响应 UIKit是iOS应用程序开发的基础,它提供了丰富的UI元素和用户交互控件,还提供了许多事件处理和动画技术。此外它还集成了多种系统服务,如文本输入、多媒体、数据存储等,以实现完整的应用程序开发流程。

Core AnimationCore Animation 其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做 Layer Kit 这么一个不怎么和动画有关的名字演变而来的,所以做动画仅仅是 Core Animation 特性的冰山一角。提供强大的 2D 和 3D 动画效果。对应到系统 Framework 中不是这个名字,而是QuartzCore.framework,以 CA 开头的都是它所属的类。 juejin.cn/post/684490…

Core GraphicsCore Graphics主要用于运行时绘制图像,纯 C 的 API。CoreGraphics 的类名都是以 CG 开头的,平时所用的 CGRectCGPoint 就在 CGGeometry 这个几何相关的类中定义,CGFont 类则被封装成了 UIFontCGImage 构成了 UIImageCGContext 是绘图的上下文等等。所以 CoreGraphics 是系统绘制界面、文字、图像等 UI 的基础。

Core Image Core Image 是用来处理运行前创建的图像 的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。给图片提供各种滤镜处理,比如高斯模糊、锐化等。在没有这个官方库之前,一般使用的是GNUImage的三方库。 大部分情况下,Core Image 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。

OpenGL(ES)OpenGL不是常规意义上的 API,而是一个第三方标准(由 khronos 组织制定并维护),其严格定义了每个函数该如何执行,以及它们的输出值。至于每个函数内部具体是如何实现的,则由 OpenGL 库的开发者自行决定。实际 OpenGL 库的开发者通常是显卡的生产商。类似的标准还有DirectX,由Microsoft提供。用在 PC 机上。 (功能:应该说在高效完成 2D/3D 界面的同时,达到了降低功耗的效果。)

Metal: Metal 类似于OpenGL ES,也是一套标准,具体实现由苹果实现。Core AnimationCore ImageSceneKitSpriteKit 等等渲染框架都是构建于 Metal 之上的。

转载自大神:juejin.cn/post/703817…