避免视图绘制掉帧

YVTU

图像显示原理

iOS 图像显示的原理涉及多个层次,从底层的硬件加速到上层的框架实现。UIKit 提供了高层次的 API,用于管理应用的用户界面元素,包括视图、控件和图像显示。UIImageView 是 UIKit 中用于显示图像的主要类。Core Graphics (也称为 Quartz) 是 iOS 的 2D 绘图引擎。它直接与硬件交互,负责图形的绘制和处理。UIKit 的许多绘图操作最终都通过 Core Graphics 实现。Core Animation 是 iOS 用来处理动画和图像渲染的框架。每个 UIView 都有一个关联的 CALayer,用于处理图像和内容的渲染。Core Animation 负责将这些图层的内容提交到屏幕上,并处理图层之间的动画。

iOS 使用 OpenGL ES 或 Metal 作为底层图形渲染管线。UIKit 和 Core Animation 会将图像、视图和动画转换为 GPU 可理解的命令,并通过 OpenGL ES 或 Metal 渲染到屏幕上。

当你使用 UIImageView 来显示图像时,以下步骤会发生:

  1. 加载图像:UIImage 类用于加载图像资源。你可以从文件系统、网络、或资源包中加载图像。
  2. 设置图像:将 UIImage 对象分配给 UIImageView 的 image 属性。
  3. 图层显示:UIImageView 是一个 UIView 子类,因此它有一个关联的 CALayer。UIImageView 会将图像设置为其图层的 contents。
  4. 渲染图像:Core Animation 将负责将 CALayer 的内容提交到 GPU 进行渲染,最后显示在屏幕上。
    以下是一个简单的示例,展示如何在 iOS 中使用 UIImageView 来显示图像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

// 创建 UIImage 对象
if let image = UIImage(named: "example.jpg") {

// 创建 UIImageView 对象
let imageView = UIImageView(image: image)

// 设置 UIImageView 的大小和位置
imageView.frame = CGRect(x: 50, y: 100, width: 200, height: 200)

// 设置内容模式
imageView.contentMode = .scaleAspectFill

// 将 UIImageView 添加到视图中
self.view.addSubview(imageView)
}
}
}

iOS 会自动缓存 UIImage 对象,以提高性能并减少内存消耗。UIImageView 使用的图像可以通过系统的图像缓存机制优化。

对于大图像,最好使用分块加载或缩略图显示以减少内存占用。可以使用 CGImageSource 来逐步加载图像。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import SwiftUI

struct ThumbnailImageView: View {
let thumbnailImage: UIImage
let fullSizeImageURL: URL

@State private var fullSizeImage: UIImage? = nil

var body: some View {
ZStack {
if let fullSizeImage = fullSizeImage {
Image(uiImage: fullSizeImage)
.resizable()
.scaledToFit()
} else {
Image(uiImage: thumbnailImage)
.resizable()
.scaledToFit()
.onAppear(perform: loadFullSizeImage)
}
}
}

private func loadFullSizeImage() {
DispatchQueue.global().async {
if let data = try? Data(contentsOf: fullSizeImageURL),
let image = UIImage(data: data) {
DispatchQueue.main.async {
self.fullSizeImage = image
}
}
}
}
}

在加载大图时使用 CGImageSource 逐步解码图片,在低分辨率时减少内存占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import UIKit

func loadImageWithLowMemoryUsage(url: URL) -> UIImage? {
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
return nil
}

let options: [NSString: Any] = [
kCGImageSourceShouldCache: false, // 避免直接缓存到内存
kCGImageSourceShouldAllowFloat: true
]

return CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary).flatMap {
UIImage(cgImage: $0)
}
}

如果图像需要从网络加载,建议使用异步加载来避免阻塞主线程。可以结合使用 URLSession 或第三方库如 SDWebImage 来实现。

如果你需要自定义图像显示或进行复杂的图形处理,可以直接使用 Core Graphics。以下是一个使用 Core Graphics 绘制图像的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import UIKit

class CustomView: UIView {

override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }

// 设置填充颜色
context.setFillColor(UIColor.red.cgColor)
context.fill(rect)

// 加载图片
if let image = UIImage(named: "example.jpg")?.cgImage {
context.draw(image, in: rect)
}
}
}

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let customView = CustomView(frame: self.view.bounds)
self.view.addSubview(customView)
}
}

CPU 和 GPU 分别做了什么

CPU 做的事情:

  • 视图层次结构的计算(View Hierarchy Calculation): CPU 负责计算视图的层次结构,包括视图的位置、尺寸、透明度和其他属性。每当视图的这些属性发生变化时,CPU 会重新计算这些信息。
  • 布局(Layout): 当使用 Auto Layout 或手动布局时,CPU 会计算所有视图的位置和尺寸。比如调用 layoutSubviews() 方法时,CPU 会参与布局计算。
  • 文本绘制(Text Rendering): 如果你在视图中绘制文本,比如使用 UILabel 或 CATextLayer,CPU 会负责对文本进行排版和绘制。
  • 图像解码(Image Decoding): 从网络或磁盘加载的图像通常是经过压缩的格式(如 PNG、JPEG)。CPU 负责将这些图像解码为可供 GPU 使用的原始位图格式。
  • Core Graphics 绘制(Core Graphics Drawing): 使用 CGContext 进行的所有绘制操作(如绘制路径、填充颜色等)都是在 CPU 上执行的。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CustomView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
// CPU 负责布局计算
let subview = UIView(frame: CGRect(x: 10, y: 10, width: 100, height: 50))
addSubview(subview)
}

override func draw(_ rect: CGRect) {
super.draw(rect)
// CPU 负责文本绘制
let text = "Hello, World!"
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 16),
.foregroundColor: UIColor.black
]
text.draw(in: rect, withAttributes: attributes)
}
}

GPU 做的事情:

  • 图层合成(Layer Composition): GPU 负责将各个图层(如 UIView 或 CALayer)进行合成和渲染。每个视图或图层都会被转换为一个纹理,GPU 会将这些纹理组合到最终的帧缓冲区中。
  • 图像渲染(Image Rendering): GPU 负责渲染已经由 CPU 解码的图像位图。它还负责处理各种图像特效,如模糊、阴影、透明度等。
  • 动画处理(Animations Handling): iOS 中的大多数动画(如 UIView 动画和 Core Animation)都是在 GPU 上执行的。GPU 负责处理动画的中间帧渲染。
  • OpenGL/Metal 绘制: 当使用 OpenGL ES 或 Metal 直接进行图形绘制时,GPU 会负责执行这些绘制命令。
1
2
3
4
UIView.animate(withDuration: 1.0) {
self.view.alpha = 0.5
// 这段代码的动画渲染由 GPU 处理
}

通常,CPU 负责准备数据,GPU 负责渲染这些数据。为了优化性能,我们应该尽量减少 CPU 的负担,减少复杂的布局计算、避免频繁的视图层次变更,以及尽量延迟图像解码等操作。

优化建议:

  • 尽量简化视图层次结构: 过多的子视图会增加 CPU 的计算负担。
  • 减少过度的重绘: 尽量避免频繁调用 setNeedsDisplay 或 layoutSubviews。
  • 使用异步图像加载和解码: 避免在主线程上进行图像解码,以减轻 CPU 的负担。
  • 合理使用图像缓存: 尽量使用适合尺寸的图片,避免 GPU 处理过大图像带来的额外开销。

UIView 的绘制原理

UIView 是所有用户界面元素的基础。每个 UIView 对象都负责显示内容,并响应用户输入。UIView 的绘制过程涉及多个步骤,从层级视图系统到 Core Graphics 和 Core Animation。

UIView 是基于层次结构的,每个 UIView 都可以有多个子视图 (subviews)。所有的视图都由一个根视图 (root view) 管理,通常是 UIViewController 的 view 属性。

绘制流程:

  • 当需要更新界面时(如屏幕首次渲染、视图内容更新、视图大小变化等),系统会触发视图的绘制过程。
  • 系统调用视图的 setNeedsDisplay 或 setNeedsLayout 方法标记视图为“需要更新”状态。
  • 系统在下一个运行循环中调用 drawRect: 方法绘制视图。

UIView 的绘制是通过 Core Graphics 来完成的。Core Graphics 是一个 2D 绘图引擎,可以进行图形绘制、图像处理等。Core Animation 负责将 UIView 的内容显示在屏幕上。它将所有的绘制操作作为动画图层 (CALayer`) 的更新并进行合成,最后呈现给用户。

UIView 绘制流程的关键步骤:

  • setNeedsDisplay 和 setNeedsLayout:
    • setNeedsDisplay:标记视图为需要重绘,会在下一次屏幕刷新时调用 drawRect: 方法。
    • setNeedsLayout:标记视图需要重新布局,会在下一次布局周期调用 layoutSubviews 方法。
  • drawRect::
    • drawRect: 方法是 UIView 自定义绘制的入口。在这里,可以使用 Core Graphics 进行图形绘制。

以下是一个自定义 UIView 的示例,它在屏幕上绘制一个简单的矩形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import UIKit

class CustomView: UIView {

// 仅在初始化时调用一次
override init(frame: CGRect) {
super.init(frame: frame)
// 设置视图的一些属性,例如背景颜色
self.backgroundColor = UIColor.white
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}

// 重写 drawRect: 方法来执行自定义绘制。使用 Core Graphics API 来绘制一个蓝色的矩形。
override func draw(_ rect: CGRect) {
super.draw(rect)

// 获取当前的绘图上下文,这是一个 Core Graphics 的 `CGContext` 对象,它代表着一个绘制环境。
guard let context = UIGraphicsGetCurrentContext() else { return }

// 设置填充颜色
context.setFillColor(UIColor.blue.cgColor)

// 创建一个矩形
let rectangle = CGRect(x: 50, y: 50, width: 200, height: 100)

// 在上下文中绘制矩形
context.addRect(rectangle)
context.drawPath(using: .fill)
}
}

// 使用自定义视图
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

// 创建并添加自定义视图
let customView = CustomView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
self.view.addSubview(customView)
}
}

UIView 的绘制原理核心是基于 Core Graphics 和 Core Animation,通过 drawRect: 方法进行自定义绘制。这个过程涉及到多个步骤,包括视图层次结构、绘制流程、上下文获取,以及最终的内容合成并呈现给用户。在实际开发中,自定义绘制的 UIView 往往用于实现特殊的图形效果或自定义 UI 控件。

异步绘制

异步绘制主要指的是在后台线程中处理绘制操作,以避免阻塞主线程。Core Graphics 和 UIKit 提供了支持异步绘制的功能。

使用 Core Graphics 的异步绘制。使用 UIGraphicsBeginImageContextWithOptions 函数在后台线程中创建一个 CGContext。使用 GCD 或 NSOperationQueue 来在后台线程中进行绘制操作。完成绘制后,将结果返回主线程以更新 UI。
下面是一个异步绘制的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import UIKit

class AsyncDrawingView: UIView {

private var asyncImage: UIImage?

override func draw(_ rect: CGRect) {
super.draw(rect)

// 如果有异步绘制的图片,直接绘制它
asyncImage?.draw(in: rect)
}

func drawAsync() {
Task {
// 创建图形上下文
let size = self.bounds.size
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else { return }

// 进行绘制操作
context.setFillColor(UIColor.blue.cgColor)
context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))

// 获取绘制结果
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

// 更新 UI,回到主线程
await MainActor.run {
self.asyncImage = image
self.setNeedsDisplay() // 触发 draw(_:) 方法重新绘制
}
}
}
}

使用 UIKit 的异步绘制

对于复杂的异步绘制,特别是涉及 UIView 的情况下,可以考虑这两个方法。首先是自定义 CALayer 并实现其 draw(in:) 方法来进行异步绘制。其次是使用 UIView 的 draw(:) 方法,在子类中重写 draw(:) 方法,并结合异步操作来更新绘制内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import UIKit

class AsyncDrawingLayer: CALayer {

override func draw(in ctx: CGContext) {
super.draw(in: ctx)

Task {
// 在子线程中执行绘制操作
await withCheckedContinuation { continuation in
Task.detached {
// 执行绘制操作
ctx.setFillColor(UIColor.red.cgColor)
ctx.fill(self.bounds)

// 完成绘制操作后继续
continuation.resume()
}
}

// 回到主线程更新 UI
await MainActor.run {
self.setNeedsDisplay() // 触发 draw(in:) 重新绘制
}
}
}
}

离屏渲染

离屏渲染(Offscreen Rendering)在 iOS 中是指在显示器显示内容之前,先在内存中完成渲染。这种方式的主要代价体现在以下几个方面:

  • 内存消耗:离屏渲染会占用额外的内存,因为每次渲染都会创建一个新的图像缓冲区(即离屏缓存)。这些缓存可能会迅速消耗大量内存,特别是对于复杂的视图或高分辨率的图像。
  • 处理时间:进行离屏渲染时,CPU 和 GPU 需要额外的时间来处理渲染操作。这可能会导致渲染时间增加,影响到应用的流畅性。
  • 上下文切换:离屏渲染需要将渲染操作从主线程转移到后台线程或额外的图形上下文,这增加了上下文切换的开销。
  • 绘制次数:每次需要重新绘制时(例如视图的内容更新),都需要执行离屏渲染,这可能导致频繁的渲染操作,从而影响性能。

产生离屏渲染的常见原因有:

  • 圆角(Corner Radius):如果在视图或图层上应用圆角效果,iOS 需要在离屏缓存中完成这些操作,尤其是当圆角半径较大时。
  • 阴影(Shadow):阴影效果通常需要离屏渲染,因为阴影需要在原始视图之外的区域进行处理。
  • 透明度(Opacity):对于半透明的视图或图层,iOS 需要进行离屏渲染以正确处理混合效果。
  • 复杂的图形操作:比如路径(UIBezierPath)的复杂绘制,也可能导致离屏渲染。

避免离屏渲染的优化。如果视图的内容不会频繁变化,可以将视图渲染到 layer.contents 中,从而避免每次绘制时的离屏渲染开销。尽量避免复杂的圆角、阴影效果,或者使用更简单的图形操作。如可能,减少对 layer 的属性设置,尤其是那些可能引起离屏渲染的属性。