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

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

特性 UIKit SwiftUI
视图定义与生命周期 视图为类(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 系统:给"视图模板"注水

swift 复制代码
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

swift 复制代码
// ❌ 错误示范:切换分支时 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 重算粒度实验

只写不读 → 跳过

swift 复制代码
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]

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

swift 复制代码
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"别算我"

swift 复制代码
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() 声明模型
swift 复制代码
@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 用 @Observableprice 单独标记;
  • 行视图实现 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 不再是"玄学",而是可推导、可度量、可优化的纯函数式渲染管道。

参考资料 & 工具

相关推荐
pixelpilot11 小时前
Nimble:让SwiftObjective-C测试变得更优雅的匹配库
开发语言·其他·objective-c·swift
大熊猫侯佩16 小时前
张真人传艺:Swift 6.2 Actor 隔离协议适配破局心法
swiftui·swift·apple
Dream_Ji2 天前
Swift入门(二 - 基本运算符)
服务器·ssh·swift
HarderCoder3 天前
Swift 6.1 `withTaskGroup` & `withThrowingTaskGroup` 新语法导读
ios·swift
HarderCoder3 天前
Swift 并发:Actor、isolated、nonisolated 完全导读
ios·swift
用户093 天前
Swift Feature Flags:功能切换的应用价值
面试·swiftui·swift
HarderCoder3 天前
Swift 5.9 `consume` 操作符:一次说清楚“手动结束变量生命周期”
swift
YungFan4 天前
iOS26适配指南之UIScrollView
ios·swift
HarderCoder4 天前
SwiftUI Preferences 完全指南:从“向上传值”到 Swift 6 并发安全
swiftui·swift