本文主要探究两个问题。
- SwiftUI是如何把View渲染到屏幕上的?
- 通过数据驱动触发页面渲染的过程是怎么样的?
SwiftUI的核心在于通过一个View类型的树状结构来描述页面应该呈现什么样子,通过改变状态(state),SwiftUI会重新计算新的树状结构,从而在屏幕上呈现相应的改动内容。
这个过程大体可以分为以下几个阶段。
- 声明式语法 Declarative Syntax
- 视图构建 View Construction
- 视图树类型推导 View Type Composition
- 视图对比(Diffing)
- 渲染树生成 Rendering Tree / View Graph
- Platform View Updates → UIKit/AppKit 层
- 屏幕渲染 Frame Drawing
其中5到7对于我们来讲不可见,本文只讨论1至4部分。
让我们通过一个简单的例子来理解这些过程:
这个页面通过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预览 我们就得到了如下打印信息。
可以看到VStack下多出了一个
TupleView
,而处于 if counter > 0
判断下的Text
本应不可见,但此时显现出来了且变成了Optional<Text>
,这里就显现出了第三个阶段视图树类型推导(View Type Composition)
通过VStack的定义可以观察到,在尾随闭包上有一个@ViewBuilder
的标识,它的核心功能就是:将你写的多行 View,组合成一个类型安全的 View 类型。
而ViewBuilder
的核心则在于顶部声明的@resultBuilder
及一系列的静态buildBlock
函数
因此,当我们写下:
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全量渲染
从打印中可以看到,除了首次启动的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