SwiftUI 渲染原理探索

本文主要探究?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è)階段。

  1. 聲明式語(yǔ)法 Declarative Syntax
  2. 視圖構(gòu)建 View Construction
  3. 視圖樹(shù)類(lèi)型推導(dǎo) View Type Composition
  4. 視圖對(duì)比(Diffing)
  5. 渲染樹(shù)生成 Rendering Tree / View Graph
  6. Platform View Updates → UIKit/AppKit 層
  7. 屏幕渲染 Frame Drawing

其中5到7對(duì)于我們來(lái)講不可見(jiàn),本文只討論1至4部分。

讓我們通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)理解這些過(guò)程:

截屏2025-05-19 21.51.52.png

這個(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ù)覽 我們就得到了如下打印信息。

截屏2025-05-19 22.03.24.png

可以看到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)

截屏2025-05-19 22.28.44.png

通過(guò)VStack的定義可以觀察到,在尾隨閉包上有一個(gè)@ViewBuilder的標(biāo)識(shí),它的核心功能就是:將你寫(xiě)的多行 View,組合成一個(gè)類(lèi)型安全的 View 類(lèi)型。

ViewBuilder的核心則在于頂部聲明的@resultBuilder及一系列的靜態(tài)buildBlock函數(shù)

截屏2025-05-19 22.31.49.png

因此,當(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全量渲染

截屏2025-05-20 00.52.54.png

從打印中可以看到,除了首次啟動(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容