本文涉及的闭包、变量传递和引用捕获的逻辑。整体过程涉及 ContentView 中的闭包传递 和 EditView 中的闭包使用,以及它们的内存与捕获关系。
闭包示例
在ContentView视图中,当长按地标后,调用Sheet页:
.sheet(item: $selectedPlace) { place in
EditView(location: place) { newLocation in
print("Updating \(newLocation)")
if let index = locations.firstIndex(of: place) {
locations[index] = newLocation
}
}
}
Sheet绑定的selectedPlace是一个@State对象,该对象用于存储选择的地标实例:
@State private var selectedPlace: Location?
Location是一个结构体:
struct Location: Codable, Equatable, Identifiable { }
EditView视图初始化器为一个location变量和一个onSave闭包:
struct EditView: View {
var location: Location
var onSave: (Location) -> Void
@State private var name: String
@State private var description: String
init(location: Location,onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave
_name = State(initialValue: location.name)
_description = State(initialValue: location.description)
}
...
Button("Save") {
var newLocation = location
newLocation.id = UUID()
newLocation.name = name
newLocation.description = description
onSave(newLocation)
dismiss()
}
}
闭包被传递给EditView后,存储在onSave属性中,当点击Button按钮时,会将修改后的location属性引用的对象传入闭包,从而完成闭包在视图中的传递和使用。
示例图
代码解析
@State private var selectedPlace: Location?
.sheet(item: $selectedPlace) { place in
EditView(location: place) { newLocation in
print("Updating \(newLocation)")
if let index = locations.firstIndex(of: place) {
locations[index] = newLocation
}
}
}
1、Sheet视图的创建和传递
@State private var selectedPlace: Location?
.sheet(item: $selectedPlace) { place in
...
}
@State private var selectedPlace: Location? 是一个可选类型的状态变量,表示当前选中的位置。
当 selectedPlace 被赋值一个非 nil 的值时,sheet 会检测到绑定值的变化,并展示一个 Sheet 视图。
sheet 的 item 参数是一个绑定(Binding),其核心是双向数据流。当 selectedPlace 的值变化时,sheet 视图会随之更新。反之,关闭 Sheet 时,SwiftUI 会将 selectedPlace 重置为 nil。
在 Sheet 内部,place 是由 selectedPlace 的当前值拷贝生成的局部变量,用于提供给 Sheet 内容视图的闭包。
2、SwiftUI 的 @State 管理机制
1)@State 的存储管理:
@State 标注的变量并非直接存储在 View 本身,而是被 SwiftUI 存储在堆内存的一个外部结构中,用以管理状态的生命周期。
SwiftUI 会自动为 @State 变量生成一个前缀为 _ 的属性(如 _selectedPlace),它是 Binding 的实际实现,包含对状态值的引用。
2)访问机制:
selectedPlace 提供了对状态值的访问器,访问其 wrappedValue 会读取或写入存储在堆内存中的实际值。
selectedPlace的读取和写入是通过@State背后的存储机制实现的。
selectedPlace通过_selectedPlace.wrappedValue提供的接口,来实现对状态值的访问,换句话说,selectedPlace是_selectedPlace.wrappedValue的一个快捷方式。
在 sheet 的绑定中,$selectedPlace 是将 selectedPlace 转换为 Binding 类型,它封装了对 wrappedValue 的读写操作,使 SwiftUI 能够感知状态变化。
通过列举selectedPlace、_selectedPlace和$selectedPlace,介绍了selectedPlace的关系,相比之下:
_selectedPlace是 State,它管理状态值并提供基础的存储与访问功能。 wrappedValue更底层,直接操作状态值。
$selectedPlace 是更高层的绑定封装,专为动态绑定场景设计,支持双向数据流。
3、Sheet 的内部逻辑
1)视图生成和更新:
当 selectedPlace 的值更新为非 nil 时,Sheet 会调用绑定闭包,生成一个新的视图。
item 是绑定到 selectedPlace 的实际值,当 selectedPlace 改变时,item 也会同步更新,Sheet 内容会随之刷新。
2)状态重置:
关闭 Sheet 时,SwiftUI 会自动将 selectedPlace 的值重置为 nil,触发 View 的重新计算。
item 不是直接绑定 $selectedPlace 的地址,而是存储 selectedPlace 的当前值(一个值类型实例),并通过 SwiftUI 的状态驱动机制,自动更新视图。
当 selectedPlace 改变时,item 会接收新的值,因此 item 存储的实际上是 selectedPlace 的快照值,而不是直接的引用地址。
这种机制符合 SwiftUI 的设计哲学:通过状态驱动视图更新,而不是依赖直接的引用或绑定地址。
4、Sheet视图内的闭包
.sheet(item: $selectedPlace) { place in
EditView(location: place) {
...
}
}
从selectedPlace回到Sheet视图本身,{ place in }是一个闭包,
sheet(item: Binding<Item?>, content: @escaping (Item) -> Content) // 省略写法
SwiftUI 会将这个闭包保存在堆内存中,指针(content)存储闭包的地址。
这样做的目的是:即使在不同的状态更新中,可以动态调用该闭包生成视图内容。
闭包执行时,以下内容发生:
在栈内存中,place 变量被创建,用于持有 item(即 selectedPlace 解包后的值)的当前快照。
捕获环境:locations 数组是闭包外部变量,它会被捕获到闭包的捕获环境中,但仅捕获引用(详解见后面的“数组的引用问题”)。
content 闭包的返回值是一个新的 EditView,该视图在堆内存中创建并存储,成为 Sheet 的一部分。
EditView 的 location 参数接收的是 place 的值。
数组的引用问题
我们在捕获locations会注意到,捕获的是引用。而locations是一个数组,数组的元素类型是Location结构体,所以数组和元素都是值类型,按理说捕获环境获取的是locations的值,发生值拷贝,而不是引用。
但 Swift 的数组有一个 优化机制,叫做“写时复制(Copy-on-Write, COW)”。它在实际操作中表现为引用语义的某些特性。
写时复制(Copy-on-Write, COW)
虽然数组是值类型,但 Swift 优化了性能,避免在每次传递数组时立即拷贝整个数组。具体行为是:
传递数组时:不会立即拷贝,而是让多个变量共享同一个底层存储(堆内存中的数组内容)。
修改数组时:如果某个共享数组被修改,Swift 会自动创建该数组的独立副本,然后对副本进行操作(此时才发生真正的拷贝)。
这种机制表现为:在读取或传递数组时,行为类似引用语义;而在修改数组时,仍然符合值类型的特性。
当捕获 locations 时,Swift 实际捕获的是 locations 本身的引用(不是底层存储的直接引用),因为 Swift 的捕获规则中:
1)值类型变量直接捕获其值(值语义)。
2)可变值类型(如数组)如果发生修改,会遵循写时复制规则,影响捕获到的变量。
捕获时的行为:
捕获 locations 的数组变量(例如在闭包中)时:
捕获的是 locations 自身,而不是底层存储的直接指针。
如果闭包内修改了 locations,Swift 会执行写时复制,生成一个新数组并修改。
数组引用结论
locations 本质是值类型,但因为:
1)它被 @State 修饰,SwiftUI 会通过状态管理封装它。
2)Swift 的数组采用写时复制机制,在某些操作下表现出类似引用语义的特性。
所以描述 locations 捕获为“引用”是合理的,但其核心还是值类型。
5、EditView视图的传递
struct EditView: View {
var location: Location
var onSave: (Location) -> Void
init(location: Location,onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave
}
...
}
EditView 的 {newLocation in}闭包被传递给EditView视图的onSave属性,这个闭包被存储在EditView的视图区域内,引用了外部变量(如locations),因此形成了捕获环境。
捕获环境的共享
1、Sheet 和 EditView 间的捕获环境:
onSave 捕获了外部的 locations。
虽然 Sheet 和 EditView 是不同的视图,但 locations 作为闭包的捕获变量,实际上引用的是同一个对象。
捕获环境是共享的,因此对 locations 的修改能被所有持有该捕获环境的地方感知。
2、捕获行为:
locations 是数组(值类型),但因为它是 @State 属性的存储,SwiftUI 的状态管理会将其包装在引用类型中。
因此,闭包捕获的是 locations 的引用(底层是状态存储对象),保证能追踪数组的修改。
当在EditView视图中点击Button(“Save”)按钮时:
Button("Save") {
var newLocation = location
newLocation.id = UUID()
newLocation.name = name
newLocation.description = description
onSave(newLocation)
dismiss()
}
1、栈上的 newLocation 变量:
点击按钮时,Swift 会在栈上创建一个 newLocation 变量。
newLocation 是对 location 的拷贝。
2、修改 newLocation:
在闭包中对 newLocation 的修改不会影响 location,因为它们是不同的实例。
3、调用 onSave 闭包:
onSave(newLocation)
调用时,newLocation 的值被拷贝传递到 onSave。
在 onSave 内部,闭包的 newLocation 是一个独立的栈变量,但值与按钮点击中生成的 newLocation 相同。
6、onSave 闭包内的执行逻辑
{ newLocation in
print("Updating \(newLocation)")
if let index = locations.firstIndex(of: place) {
locations[index] = newLocation
}
}
1、修改 locations:
闭包内的 locations 是捕获的外部变量,指向 SwiftUI 状态管理的包装对象。
使用 locations.firstIndex(of: place) 查找索引,修改数组的对应元素。
2、状态更新:
栈内存创建index变量用于保存返回的索引。
locations[index] = newLocation 会触发 SwiftUI 重新计算绑定到 locations 的视图,更新界面。
以上就是全部的知识内容,从内存、引用、闭包等多个角度分析,闭包是如何传递到各视图,以及如何调用的,知识量很多,这个代码从一步一步理解、分析到完整的写完这个教程,差不多也花费了我近4天的时间,希望看完该教材后,对更多的人有所帮助。
扩展知识
locations数组的修改
结尾数组的引用问题,在这里进一步分析:
locations[index] = newLocation
1、获取 locations 的引用
通过捕获环境引用 locations。
捕获环境存储了 locations 的引用,指向 SwiftUI 的 StateStorage 对象,实际数组存储在堆内存中。
2、检查写时复制条件
Swift 检查数组是否有其他引用共享其底层存储。
如果没有共享(例如只有一个闭包捕获了 locations),直接修改底层存储。
如果存在共享引用(比如其他视图或线程正在访问相同的 locations),Swift 执行写时复制。
3、执行修改
1、如果没有触发写时复制:
直接在数组的堆内存中,将第 index 个元素替换为 newLocation。
2、如果触发了写时复制:
创建一个新数组,将原始数组的数据复制到新数组。
在新数组中,替换第 index 个元素。
更新捕获环境中的 locations 引用,让它指向新数组。
locations修改没有触发写时复制,因此属于第一种。
是否直接修改数组索引指向的实际数组?
捕获环境中的 locations 并不是直接捕获数组本身,而是捕获 @State 的包装器对象。因此:
修改行为:实际操作的是包装器对象管理的底层数组存储。
写时复制:在包装器对象中触发,包装器对象会替换底层数组的存储。
所以从开发者的角度来看,修改是直接对数组的操作;但底层实现会根据写时复制的规则决定是否生成新数组。
locations为什么没有触发写时复制?
content闭包和onSave闭包都捕获了locations数组,存在多处引用,但还需要考虑 SwiftUI 的 @State 特性。
SwiftUI 的 @State 对引用语义的影响
在 SwiftUI 中,@State 属性包装器将值类型(如数组)包装成一种引用语义对象,通常是某种 StateStorage 类,实际数组存储在堆内存中。这样设计的目的是追踪状态变化,以便触发视图更新。
1、locations 的引用:
外层捕获环境中的 locations 和 onSave 捕获的 locations 引用的实际上是同一个 StateStorage 对象(不是直接引用数组)。
由于这两个引用指向同一个包装器对象,在默认情况下,并不会触发写时复制。
2、数组修改行为:
当执行 locations[index] = newLocation 时,操作的实际上是 StateStorage 管理的数组。
如果 StateStorage 的内部实现确保没有其他视图或状态系统共享底层数组存储,那么修改是直接在原数组上进行的,不会触发写时复制。
写时复制的触发条件
在 Swift 的写时复制机制下,触发 COW 的条件是:
1、是否有多个独立引用共享同一底层数组存储。
2、是否需要保护值类型的独立性(即保证修改后不同引用不受影响)。
但是,SwiftUI 的 @State 属性包装器破坏了值类型的独立性,因为它将数组包装成了引用语义。因此:
locations 在不同捕获环境中的引用本质上是同一个 StateStorage 对象。
修改 locations 并不会触发写时复制,而是直接在原数组的存储上进行修改。
locations[index] = newLocation 的执行过程
假设 locations 是 @State 修饰的数组:
1、查找索引:
firstIndex(of:) 查找到目标元素的索引 index。
index 是栈内存中的临时变量。
2、修改数组元素:
调用 locations[index] = newLocation。
如果只有一个引用持有底层数组存储,直接在原存储上修改。
如果存在其他共享引用,触发写时复制(但在 @State 管理下通常不会发生)。
为什么不会触发写时复制?
SwiftUI 的 @State 属性包装器和捕获环境的引用机制导致:
捕获的是引用语义包装器(StateStorage 对象),而不是直接捕获数组本身。
共享的是包装器,而不是底层数组存储,因此修改时不会触发 COW。
只有在不使用 @State,直接捕获普通数组的情况下,修改才可能触发写时复制。
关于写时复制的总结为:
1、闭包捕获的是 @State 包装器的引用,而非直接捕获值类型数组。
2、@State 包装器内部用引用语义管理值类型,因此直接修改通常不会触发写时复制。
3、只有当底层值被多个独立引用共享时,写时复制才会被触发。
为什么需要写时复制?
写时复制是为了确保值类型的语义完整性。在以下场景中会显得尤为重要:
1、多个视图共享同一个 locations:
如果不触发写时复制,修改会导致多个视图同时看到变化,违背了值类型的独立性原则。
2、线程安全:
写时复制避免了多个线程同时修改同一个数据的风险。
这种机制在保证值类型语义的同时,结合引用语义提升了性能和灵活性,是 Swift 语言的重要特性之一。