注册
iOS

SwiftUI redraw 机制全景解读:从 @State 到 Diffing

为什么 UIKit 程序员总问“我的状态去哪了?”

特性UIKitSwiftUI
视图定义与生命周期视图为类(Class),生命周期明确,长期驻留内存视图为值类型(Struct),每次刷新生成新实例
状态保存方式状态保存在视图对象内部Struct 销毁后,状态需由外部系统(如 ObservableObject、@State 等)托管

SwiftUI 提供了一堆 Property Wrapper 来“假装”状态还在视图里,核心就是 @State

@State 到底做了什么?(4 步流水线)

SwiftUI 把一次刷新拆成 4 个微观阶段:

  1. Invalidation(打脏标)

    对用到的属性插 依赖旗标;值改变时插旗为 dirty。

  2. Recompute(重算 body)

    只重算脏旗波及的 body;没读到值的 State 直接跳过。

  3. Diffing(结构差异)

    旧的 View 树 vs 新的 View 树,找出最小集合。

  4. Redraw(GPU 提交)

    Core Animation 仅把真正改动的图层提交给 GPU。

Attribute 系统:给“视图模板”注水

struct DemoView: View {
// 1️⃣ 在视图首次出现时,SwiftUI 为其创建一个持久化的存储槽位
@State private var threshold: CGFloat = 50.0 // ← 生成一个 attribute

var body: some View {
VStack { // ← 生成一个 attribute
Button("改变") {
threshold = 41.24 // 2️⃣ 写入新值 -> 生成 Transaction
}
Text("当前阈值 \(threshold)") // 3️⃣ 读取值 -> 建立依赖
}
}
}
  • Transaction:同一“事件循环”里所有 State 变化打包成一次事务。
  • Cascade Flag:只要 threshold 被打脏,所有读过它的 attribute 都会被连锁打脏。
  • Rule:body 里没读到 = 不 recomputed。官方 Instrument 里会显示body(skipped)

身份稳定:为什么“同一个”视图才能保持 State

// ❌ 错误示范:切换分支时 struct 类型相同,但身份不同 -> State 丢失
struct MyMusic: View {
@State private var rockNRoll = false
var body: some View {
VStack {
if rockNRoll {
MusicBand(name: "The Rolling Stones") // 新身份
} else {
MusicBand(name: "The Beatles") // 另一个身份
}
}
}
}

// ✅ 正确姿势:保证身份稳定(使用相同视图,只改参数)
struct MyMusic: View {
@State private var rockNRoll = false
var body: some View {
MusicBand(name: rockNRoll ? "The Rolling Stones" : "The Beatles")
}
}

口诀:

“同一视图,不同入参” 用参数传值;

“不同视图” 用 if/else 就会换身份,State 清零。

Body 重算粒度实验

只写不读 → 跳过

struct MovieDetail: View {
let movie: Movie
@State private var favoriteMovies: [String] = []

var body: some View {
VStack {
Button("加收藏") {
favoriteMovies.append(movie.name) // 只写
}
}
}
}

Instrument 显示:MovieDetail.body [skipped]

读写 → 重算,但子视图可跳过

var body: some View {
VStack {
HStack { // 👈 重算,因为读 favoriteMovies
Text(movie.name)
Image(systemName: favoriteMovies.contains(movie.name) ? "star.fill" : "star")
}
Artwork() // 👈 没传参,不 recomputed
Synopsis()
Reviews()
}
}

经验:把“纯展示”拆成无参子视图就能躲过重算。

Equatable:手动告诉 SwiftUI“别算我”

struct FlightDetail: View, Equatable {
let flightNumber: Int
let isDelayed: Bool

// 自定义相等:只看航班号
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.flightNumber == rhs.flightNumber
}

var body: some View {
VStack {
Text("航班 \(flightNumber)")
Text(isDelayed ? "延误" : "准点")
}
}
}
  • 若 struct 全是 POD 类型(Int/Bool…),SwiftUI 会跳过你的 ==,直接按位比较。
  • 想让自定义相等生效:
    1. 包一层 EquatableView(content: FlightDetail(...))
    2. 或者 .equatable() 修饰符。

@Observable vs ObservableObject:从“对象级”到“属性级”

特性Combine(旧)Observation(新)
监听机制监听 objectWillChange 发布者监听具体属性的 KeyPath 变化
更新范围任意 @Published 属性修改触发整个 body 重算仅读取了被修改属性的 body 部分重算
属性包装器要求需通过 @StateObject/@ObservedObject 管理可观察对象可直接使用 @State var model = MyModel() 声明模型
@Observable
final class Phone {
var number = "13800138000"
var battery = 100
}

struct Detail: View {
let phone: Phone // 无需 ObservedObject

var body: some View {
Text("电池 \(phone.battery)") // 只当 battery 变才重算
}
}

扩展场景:把知识用到“极端”界面

  1. 万级实时股票列表
  • Model 用 @Observable 把 price 单独标记;
  • 行视图实现 Equatable 仅对比 symbol + price
  • 收到 WebSocket 推送时只改 price,其余字段不动 → 一行只重算自己。
  1. 复杂表单(100+ 输入框)
  • 把每个字段拆成独立子视图;
  • 用 @FocusState + @Observable FormModel,保证敲一个字只重算当前 TextField
  • 提交按钮用 .equatable() 锁定,输入过程不刷新。
  1. 大图轮播 + 陀螺仪
  • @State 保存偏移;
  • 用 TimelineView 按帧读陀螺仪,但把昂贵的图片解码放到后台 Task
  • 仅当图片索引变化才改 Image(source),避免每帧 diff 大图。

个人总结:从“魔法”到“可预测”

SwiftUI 的刷新机制看似黑盒,实则高度 可确定:

“谁依赖,谁重算;谁相等,谁跳过;谁不变,谁不绘。”

把它当成一个依赖追踪引擎而非“UI 库”,就能解释所有现象:

  • 状态放对位置(身份稳定);
  • 依赖剪到最细(读多少算多少);
  • 比较给到提示(Equatable/Observable);
  • 性能用 Instrument 量化(Effect Graph + Core Animation)。

掌握这四步,SwiftUI 不再是“玄学”,而是可推导、可度量、可优化的纯函数式渲染管道。

参考资料 & 工具


作者:unravel2025
来源:juejin.cn/post/7556247403710496820

0 个评论

要回复文章请先登录注册