SwiftUI 渲染原理探索

本文主要探究两个问题。

  • SwiftUI是如何把View渲染到屏幕上的?
  • 通过数据驱动触发页面渲染的过程是怎么样的?

SwiftUI的核心在于通过一个View类型的树状结构来描述页面应该呈现什么样子,通过改变状态(state),SwiftUI会重新计算新的树状结构,从而在屏幕上呈现相应的改动内容。

这个过程大体可以分为以下几个阶段。

  1. 声明式语法 Declarative Syntax
  2. 视图构建 View Construction
  3. 视图树类型推导 View Type Composition
  4. 视图对比(Diffing)
  5. 渲染树生成 Rendering Tree / View Graph
  6. Platform View Updates → UIKit/AppKit 层
  7. 屏幕渲染 Frame Drawing

其中5到7对于我们来讲不可见,本文只讨论1至4部分。

让我们通过一个简单的例子来理解这些过程:

截屏2025-05-19 21.51.52.png

这个页面通过VStack包裹了一个HStack和一个有条件的Text,HStack中包含了一个Button。在这个过程中并不像UIKit那样创建UI对象,并管理这些UI对象的实例。整个过程只是在描述各个UI元素,通过元素的value来“说明”这个View长什么样。

当我们保存文件,通过#Preview预览可以看到视图构建(View Construction)的过程已经打印出来。

值得注意的是,“body construciting 4”由于条件不满足并没有被打印出来,但是这并不代表当次渲染不涉及到这块元素。我们可以通过一个简单的Hook函数来查看下一个阶段。

extension View {
    func debug() -> Self {
        print(Mirror(reflecting: self).subjectType)
            return self
    }
}

挂载这个debug函数后我们再次保存文件触发#Preview预览 我们就得到了如下打印信息。

截屏2025-05-19 22.03.24.png

可以看到VStack下多出了一个TupleView,而处于 if counter > 0判断下的Text本应不可见,但此时显现出来了且变成了Optional<Text>,这里就显现出了第三个阶段视图树类型推导(View Type Composition)

截屏2025-05-19 22.28.44.png

通过VStack的定义可以观察到,在尾随闭包上有一个@ViewBuilder的标识,它的核心功能就是:将你写的多行 View,组合成一个类型安全的 View 类型。

ViewBuilder的核心则在于顶部声明的@resultBuilder及一系列的静态buildBlock函数

截屏2025-05-19 22.31.49.png

因此,当我们写下:

VStack {
    Text("ABC")
    Text("DEF")
}

Swift编译器做了两件事:

  • 观察到链路中的@resultBuilder,把这个 closure 的内容展开,变为:
ViewBuilder.buildBlock(Text("ABC"), Text("DEF"))
  • 根据传了 2 个视图,调用 buildBlock函数,最终得到
TupleView<(Text, Text)>

下一个步骤就是SwiftUI中比较核心的Diffing

Diffing 是什么?

在 SwiftUI 中,你每次更新 @State、@ObservedObject 等数据源,都会触发 body 的重新计算,生成一个新的视图树。SwiftUI 会将这棵新的视图树与旧的视图树进行比较(diff),只更新那些真正发生变化的部分。

这个过程就叫做 diffing。

PS: 在第一次启动时并没有发生 diffing,因为这是首次渲染,没有可比对的“旧视图树”。

当View更新了状态,比如:

@State var count = 0

var body: some View {
    Text("Count: \(count)")
}

用户点击按钮将 count 增加为 1,则 SwiftUI 会:

  • 触发 body 重计算,生成一棵新的视图树Text("Count: 1")

  • SwiftUI 对比新旧树,发现只有Text("Count: 0")变成了 Text("Count: 1")

  • 只更新这一小部分,避免重新构建整个界面。

这个差异计算过程就是 Diffing,SwiftUI 是通过类型匹配 + identity(id)+ children顺序进行匹配判断的。

在实际开发中经常会造成的一个误解是:body全量计算≠body全量渲染

截屏2025-05-20 00.52.54.png

从打印中可以看到,除了首次启动的body计算外,每当@state @binding之类发生变化时,body属性会再次进行全量计算,但此时只是到了第2步的视图构建 View Construction阶段,并不是body内全部的元素全部都会重新渲染,只有当走完第4步后产生了diffing,才会继续往下对相对应的ui元素进行渲染。

作为开发人员能接触到的渲染流程基本总结到此,其中还有很多有意思的部分,例如swiftui是如何推断的?推断期间会生成n个解(solition)并计算得分的过程是如何运作的?由于篇幅有限,希望今后有机会可以再展开。

本文大多思路来源于《Thinking in SwiftUI》一书,非常值得花时间细细研读,推荐给大家。https://www.scribd.com/document/750961188/eidhof-chris-kugler-florian-thinking-in-swiftui-updated-for

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 原创:有趣知识点摸索型文章创作不易,请珍惜,之后会持续更新,不断完善个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔...
    时光啊混蛋_97boy阅读 4,070评论 1 11
  • SwiftUI 学习 一、为什么 SwiftUI 中的视图用 struct 而不是 class 值类型设计:Swi...
    Tuberose阅读 65评论 0 0
  • SwiftUI要求 iOS13.0+ 快捷键 control + option + 点击:出现属性编辑器 comm...
    余青松阅读 6,434评论 1 11
  • 学习文章 文集:Hacking with iOS: SwiftUI Edition[https://www.jia...
    xmb阅读 4,592评论 3 14
  • SwiftUI的设计理念 Swift UI 自2019年由Apple引入,在新版本iOS系统中被不断引入和完善,部...
    zzzworm阅读 106评论 0 0