本文主要探究?jī)蓚€(gè)問(wèn)題。
- SwiftUI是如何把View渲染到屏幕上的?
- 通過(guò)數(shù)據(jù)驅(qū)動(dòng)觸發(fā)頁(yè)面渲染的過(guò)程是怎么樣的?
SwiftUI的核心在于通過(guò)一個(gè)View類(lèi)型的樹(shù)狀結(jié)構(gòu)來(lái)描述頁(yè)面應(yīng)該呈現(xiàn)什么樣子,通過(guò)改變狀態(tài)(state),SwiftUI會(huì)重新計(jì)算新的樹(shù)狀結(jié)構(gòu),從而在屏幕上呈現(xiàn)相應(yīng)的改動(dòng)內(nèi)容。
這個(gè)過(guò)程大體可以分為以下幾個(gè)階段。
- 聲明式語(yǔ)法 Declarative Syntax
- 視圖構(gòu)建 View Construction
- 視圖樹(shù)類(lèi)型推導(dǎo) View Type Composition
- 視圖對(duì)比(Diffing)
- 渲染樹(shù)生成 Rendering Tree / View Graph
- Platform View Updates → UIKit/AppKit 層
- 屏幕渲染 Frame Drawing
其中5到7對(duì)于我們來(lái)講不可見(jiàn),本文只討論1至4部分。
讓我們通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)理解這些過(guò)程:

這個(gè)頁(yè)面通過(guò)VStack包裹了一個(gè)HStack和一個(gè)有條件的Text,HStack中包含了一個(gè)Button。在這個(gè)過(guò)程中并不像UIKit那樣創(chuàng)建UI對(duì)象,并管理這些UI對(duì)象的實(shí)例。整個(gè)過(guò)程只是在描述各個(gè)UI元素,通過(guò)元素的value來(lái)“說(shuō)明”這個(gè)View長(zhǎng)什么樣。
當(dāng)我們保存文件,通過(guò)#Preview預(yù)覽可以看到視圖構(gòu)建(View Construction)的過(guò)程已經(jīng)打印出來(lái)。
值得注意的是,“body construciting 4”由于條件不滿足并沒(méi)有被打印出來(lái),但是這并不代表當(dāng)次渲染不涉及到這塊元素。我們可以通過(guò)一個(gè)簡(jiǎn)單的Hook函數(shù)來(lái)查看下一個(gè)階段。
extension View {
func debug() -> Self {
print(Mirror(reflecting: self).subjectType)
return self
}
}
掛載這個(gè)debug函數(shù)后我們?cè)俅伪4嫖募|發(fā)#Preview預(yù)覽 我們就得到了如下打印信息。

可以看到VStack下多出了一個(gè)
TupleView,而處于 if counter > 0判斷下的Text本應(yīng)不可見(jiàn),但此時(shí)顯現(xiàn)出來(lái)了且變成了Optional<Text>,這里就顯現(xiàn)出了第三個(gè)階段視圖樹(shù)類(lèi)型推導(dǎo)(View Type Composition)

通過(guò)VStack的定義可以觀察到,在尾隨閉包上有一個(gè)@ViewBuilder的標(biāo)識(shí),它的核心功能就是:將你寫(xiě)的多行 View,組合成一個(gè)類(lèi)型安全的 View 類(lèi)型。
而ViewBuilder的核心則在于頂部聲明的@resultBuilder及一系列的靜態(tài)buildBlock函數(shù)

因此,當(dāng)我們寫(xiě)下:
VStack {
Text("ABC")
Text("DEF")
}
Swift編譯器做了兩件事:
- 觀察到鏈路中的
@resultBuilder,把這個(gè) closure 的內(nèi)容展開(kāi),變?yōu)椋?/li>
ViewBuilder.buildBlock(Text("ABC"), Text("DEF"))
- 根據(jù)傳了 2 個(gè)視圖,調(diào)用
buildBlock函數(shù),最終得到
TupleView<(Text, Text)>
下一個(gè)步驟就是SwiftUI中比較核心的Diffing。
Diffing 是什么?
在 SwiftUI 中,你每次更新 @State、@ObservedObject 等數(shù)據(jù)源,都會(huì)觸發(fā) body 的重新計(jì)算,生成一個(gè)新的視圖樹(shù)。SwiftUI 會(huì)將這棵新的視圖樹(shù)與舊的視圖樹(shù)進(jìn)行比較(diff),只更新那些真正發(fā)生變化的部分。
這個(gè)過(guò)程就叫做 diffing。
PS: 在第一次啟動(dòng)時(shí)并沒(méi)有發(fā)生 diffing,因?yàn)檫@是首次渲染,沒(méi)有可比對(duì)的“舊視圖樹(shù)”。
當(dāng)View更新了狀態(tài),比如:
@State var count = 0
var body: some View {
Text("Count: \(count)")
}
用戶(hù)點(diǎn)擊按鈕將 count 增加為 1,則 SwiftUI 會(huì):
觸發(fā) body 重計(jì)算,生成一棵新的視圖樹(shù)
Text("Count: 1")。SwiftUI 對(duì)比新舊樹(shù),發(fā)現(xiàn)只有
Text("Count: 0")變成了Text("Count: 1")。只更新這一小部分,避免重新構(gòu)建整個(gè)界面。
這個(gè)差異計(jì)算過(guò)程就是 Diffing,SwiftUI 是通過(guò)類(lèi)型匹配 + identity(id)+ children順序進(jìn)行匹配判斷的。
在實(shí)際開(kāi)發(fā)中經(jīng)常會(huì)造成的一個(gè)誤解是:body全量計(jì)算≠body全量渲染

從打印中可以看到,除了首次啟動(dòng)的body計(jì)算外,每當(dāng)@state @binding之類(lèi)發(fā)生變化時(shí),body屬性會(huì)再次進(jìn)行全量計(jì)算,但此時(shí)只是到了第2步的視圖構(gòu)建 View Construction階段,并不是body內(nèi)全部的元素全部都會(huì)重新渲染,只有當(dāng)走完第4步后產(chǎn)生了diffing,才會(huì)繼續(xù)往下對(duì)相對(duì)應(yīng)的ui元素進(jìn)行渲染。
作為開(kāi)發(fā)人員能接觸到的渲染流程基本總結(jié)到此,其中還有很多有意思的部分,例如swiftui是如何推斷的?推斷期間會(huì)生成n個(gè)解(solition)并計(jì)算得分的過(guò)程是如何運(yùn)作的?由于篇幅有限,希望今后有機(jī)會(huì)可以再展開(kāi)。
本文大多思路來(lái)源于《Thinking in SwiftUI》一書(shū),非常值得花時(shí)間細(xì)細(xì)研讀,推薦給大家。https://www.scribd.com/document/750961188/eidhof-chris-kugler-florian-thinking-in-swiftui-updated-for