一、SwiftUI的声明式UI本质
SwiftUI的本质是"UI即状态的函数"——给定状态,产出视图描述(View Tree)。这个描述是值类型(Value Type),每次状态变更都会生成一棵新的视图树。框架通过比对新旧两棵树(Diffing),计算出最小更新集(Minimal Update Set),只渲染实际变化的部分。
1.1 View Tree 的 Diffing 机制
// SwiftUI的视图构建是声明式的
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)") // count 变化时,这行会更新
Button("Increment") {
count += 1 // 状态变更 → 新View Tree → Diff → 局部更新
}
}
}
}
// SwiftUI内部执行流程(编译时生成):
// 1. combineLatest(@Published count, ...) // 监听count变化
// 2. body 被调用,生成新的 ViewDescription(结构化描述,非真实UI)
// 3. ViewDescription 与上一次的进行对比(Tree Diff)
// 4. 计算出 delta(变化的部分)
// 5. 将 delta 提交给 Renderer(Metal-backed)
// 6. Metal 绘制变更的区域(增量绘制,不是全量重绘)
1.2 Equatable 的关键作用
SwiftUI通过判断 View 的 Equatable 实现来决定是否需要更新。如果两个状态相等,就跳过重新渲染:
// 错误:没有实现Equatable,每次都重新渲染
struct UserCard: View {
let user: User // User可能很大,但diffing只看引用
var body: some View {
VStack {
Text(user.name)
Text(user.email)
}
// 问题:如果user.name没变但user.email变了,整个Card还是会重建
}
}
// 正确:精确控制diffing粒度
struct UserCard: View {
let user: User
var body: some View {
VStack {
Text(user.name) // 仅当name实际变化时才更新
.id(user.name) // Text视图用name作为identity
Text(user.email) // 仅当email变化时才更新
.id(user.email)
}
}
}
// 更进一步:视图级别Equatable
struct UserCard: View, Equatable {
let user: User
static func == (lhs: UserCard, rhs: UserCard) -> Bool {
lhs.user.name == rhs.user.name && // 精确对比
lhs.user.email == rhs.user.email &&
lhs.user.avatarURL == rhs.user.avatarURL
}
}
二、视图身份与识别机制
2.1 View Identity 冲突
SwiftUI 使用显式身份(通过 .id())和隐式身份(View Tree 位置)来追踪视图。当两者冲突时,会导致视图被意外销毁重建:
// 问题案例:ForEach使用index作为身份(index变化导致重建)
struct BadExample: View {
@State private var items = ["A", "B", "C"]
var body: some View {
VStack {
ForEach(items.indices, id: \.self) { index in // index作为ID
Text(items[index]) // 插入新元素时,index重排,原有视图被重建
}
Button("Add Item") {
items.insert("X", at: 0) // 插入到开头,index重排
}
}
}
}
// 解决方案:使用稳定唯一ID
struct GoodExample: View {
@State private var items = [
Item(id: UUID(), name: "A"),
Item(id: UUID(), name: "B"),
]
var body: some View {
VStack {
ForEach(items) { item in // Item 需实现 Identifiable
Text(item.name) // name变化时仅重建Text,不是整个列表
}
}
}
}
// 删除中间项时:只需要移动B和C的position,不是销毁重建
// 这是SwiftUI视图复用的关键原理
三、渲染性能优化实战
3.1 惰性加载(Lazy Loading)
LazyVStack/LazyHStack 只渲染屏幕可见范围内的视图,是长列表的必备优化:
// 基础LazyVStack
struct ItemList: View {
let items: [Item]
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
// SwiftUI自动计算可见范围
// 只实例化可见项的视图对象
// 滚动时,移出屏幕的视图被回收(view pool)
}
}
}
}
}
// 网格布局:LazyVGrid
struct ItemGrid: View {
let items: [Item]
private let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(items) { item in
ItemCell(item: item)
}
}
}
}
}
3.2 减少视图重建的优先级控制
// @StateObject vs @ObservedObject vs @State
// @State: 值拥有,适用于简单状态
// @StateObject: 对象拥有,适用于引用类型(class),对象创建时初始化一次
// @ObservedObject: 不拥有,适用于父视图传递进来的对象
// 常见错误:在body中创建ObservableObject(每次渲染都新建)
struct BadView: View {
// 错误:每次body重新计算都创建新的ViewModel
var body: some View {
ListView(viewModel: ListViewModel()) // ❌ ViewModel是class,重新创建浪费
}
}
// 正确:用@StateObject拥有ViewModel
struct GoodView: View {
@StateObject private var viewModel = ListViewModel() // ✅ 初始化一次
var body: some View {
ListView(viewModel: viewModel)
}
}
// @environmentObject: 用于深度传递共享的ViewModel
// 避免中间层每个View都声明@ObservedObject
3.3 避免约束条件下的重复计算
SwiftUI的视图体(body)是"计算属性",每次父视图状态变化时,body都会被重新调用。昂贵的计算放在 body 之外:
struct ExpensiveView: View {
let items: [ExpensiveItem]
@State private var searchText = ""
// ✅ computed属性在body外缓存,不重复计算
private var filteredItems: [ExpensiveItem] {
// 这个计算只在这里调用,items变化时重新计算
items.filter { item in
// 假设这个过滤很昂贵
item.name.localizedCaseInsensitiveContains(searchText)
}
}
// ✅ 计算属性缓存(items或searchText变化时才重新计算)
private var sortedItems: [ExpensiveItem] {
filteredItems.sorted { $0.name < $1.name }
}
var body: some View {
// ❌ 不要在body内直接写计算
// ForEach(items.filter { ... }.sorted { ... }) ← filter和sort每次都执行
// ✅ 使用缓存的计算属性
ForEach(sortedItems) { item in
ItemRow(item: item)
}
}
}
四、列表滚动性能优化
4.1 Prefetching 预加载
// 使用LazyVStack的onAppear优化(滚动加载更多)
struct InfiniteListView: View {
@StateObject private var viewModel = PaginatedListViewModel()
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(viewModel.items) { item in
ItemRow(item: item)
.onAppear {
// 当前行出现时检查是否需要加载更多
if item == viewModel.items.last {
Task {
await viewModel.loadNextPage()
}
}
}
}
// 加载指示器
if viewModel.isLoading {
ProgressView()
.padding()
}
}
}
}
}
// 大型图片列表的内存优化
struct OptimizedImageRow: View {
let imageURL: URL?
var body: some View {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
.clipped() // 裁剪超出区域
case .failure:
Image(systemName: "photo")
.foregroundColor(.gray)
@unknown default:
EmptyView()
}
}
}
}
五、内存与视图回收
5.1 PreferenceKey 实现精确布局
PreferenceKey 允许子视图向父视图传递测量数据,用于复杂的手动布局:
// 场景:多列布局中,每列高度不同,需要统一对齐
struct ColumnHeightPreference: PreferenceKey {
static var defaultValue: [Int: CGFloat] = [:]
static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
value.merge(nextValue(), uniquingKeysWith: max) // 保留最大高度
}
}
struct SelfSizingColumn: View {
let index: Int
let content: String
var body: some View {
Text(content)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(
GeometryReader { geo in
Color.clear.preference(
key: ColumnHeightPreference.self,
value: [index: geo.size.height]
)
}
)
}
}
// 父视图读取所有列高度
struct MultiColumnView: View {
@State private var heights: [Int: CGFloat] = [:]
var body: some View {
HStack(alignment: .top, spacing: 16) {
ForEach(0..<3, id: \.self) { index in
SelfSizingColumn(index: index, content: "Column \(index)")
}
}
.onPreferenceChange(ColumnHeightPreference.self) { newHeights in
heights = newHeights // 收集所有列的高度
}
}
}
SwiftUI性能优化清单
- 所有@State/@Published对象实现Equatable,精确触发更新
- ForEach使用稳定唯一ID,不用index作为身份
- 大型列表必须使用LazyVStack/LazyVGrid
- 避免在body内直接创建ObservableObject实例
- 大图片使用AsyncImage + .resizable(),不要在body中decode
- @StateObject在父视图创建,@ObservedObject接收父视图传递的对象