内存的访问,发生在给变量赋值的时候,或者传递值(给函数)的时候,例如
var one = 1//向one的内存区域发起一次写的操作
print("\(one)")//向one的内存区域发起一次读的操作
在 Swift 里,有很多修改值的行为都会持续好几行代码,在修改值的过程中进行访问是有可能发生的。读和写访问的区别很明显:一个写访问会改变存储地址,而读操作不会。存储地址是指向正在访问的东西(例如一个变量,常量或者属性)的位置的值 。内存访问的时长要么是瞬时的,要么是长期的。如果一个访问不可能在其访问期间被其它代码访问,那么就是一个瞬时访问。正常来说,两个瞬时访问是不可能同时发生的。大多数内存访问都是瞬时的。例如,下面列举的所有读和写访问都是瞬时的:
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// 打印“2”
然而,有几种被称为长期访问的内存访问方式,会在别的代码执行时持续进行。瞬时访问和长期访问的区别在于别的代码有没有可能在访问期间同时访问,也就是在时间线上的重叠。一个长期访问可以被别的长期访问或瞬时访问重叠。如果发生了重叠访问,就可能会造成内存冲突,因为在同一块内存区域同时发生读和写的操作是肯定不被允许的。
重叠的访问主要出现在使用 in-out 参数的函数和方法或者结构体的 mutating 方法里。
In-Out 参数的访问冲突
一个函数会对它所有的 in-out 参数保持长期写访问。in-out 参数的写访问会在所有非 in-out 参数处理完之后开始,直到函数执行完毕为止。如果有多个 in-out 参数,则写访问开始的顺序与参数的顺序一致。
这种长期保持的写访问带来的问题是,你不能再访问以 in-out 形式传入的原始变量,即使作用域原则和访问权限允许——任何访问原始变量的行为都会造成冲突。例如:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
print(number)//不会产生读访问
}
increment(&stepSize)
// 错误:stepSize 访问冲突
代码中stepSize是一个全局变量,并且我们以inout的形式将该变量传入了increment函数当中,这里我们得知道的是,一个函数会对它所有的 in-out 参数保持长期写访问,inout参数的写访问会在所有非 in-out 参数处理完之后开始,直到函数执行完毕为止。这种方法的带来的弊端就是,你不能在访问以inout形式传入的原始变量(我都得到地址了,我还访问原始干嘛,对吧),即使作用域原则和访问权限允许——任何访问原始变量的行为都会造成冲突。上述代码中,由于inout带来的长期写访问,而我们也对stepSize进行了读访问。所以就会造成内存冲突。(当然,同一函数传入两个inout相同的数据,也会造成写访问冲突)但是为什么print(number)不回发生内存冲突呢?实际上,将inout传入进去时,swift发生了以下操作
1. 写访问发生在调用期间:
• 变量 stepSize 被作为 inout 参数传递给 increment。
• Swift 会暂时暂停对原始变量的所有访问,并将其访问权转移给函数的 inout 参数 number。
2. 对原变量的隔离:
• 在函数调用期间,stepSize 被隔离,外部无法访问。
• 任何对 number 的读写操作都会作用于 stepSize 的值,但此时只通过 number 访问变量。
3. 生命周期的非交叠:
• 函数调用开始时,stepSize 的值被写入到 number 的内存。
• 在函数调用结束时,number 的值被写回到 stepSize。
• 在调用期间,stepSize 不再直接可用,因此不存在交叠访问。
解决冲突的方式很多,这里举例一种很鸡肋的方式:
// 显式拷贝
var copyOfStepSize = stepSize
increment(©OfStepSize)
// 更新原来的值
stepSize = copyOfStepSize
// stepSize 现在的值是 2
方法里self带来访问冲突
废话少说,上图
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // 正常
oscar.shareHealth(with: &Oscar) // 错误:oscar 访问冲突
在上述代码中我们在plyer结构题方法中对restoreHealth使用mutating访问权限,这样就会一直有对self的写访问,而在oscar.shareHealth我们传入了oscar,shareHealth中的inout又对Player进行的写访问,就造成了两个写访问冲突。所以会造成内存
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 错误
崩溃。有人疑惑为什么上面的restoreHealth()中不会发生内存冲突呢?
我们可以看到,maxHealth我们是是用static静态修饰的,他属于类型本身,而不属于任何实例,他储单独存储在静态区域。所以访问自然与实例无关了。
属性的访问冲突
如结构体,元组和枚举的类型都是由多个独立的值组成的,例如结构体的属性或元组的元素。因为它们都是值类型,修改值的任何一部分都是对于整个值的修改,意味着其中一个属性的读或写访问都需要访问一整个值。例如,元组元素的写访问重叠会产生冲突:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 错误:playerInformation 的属性访问冲突
在结构体中,下面展示了对于一个存储在全局变量里的结构体属性的写访问重叠了。
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 错误
但是对于本地变量,编译器就可以,编译器就可以保证重叠访问是安全的(不懂但尊重)。
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // 正常
}
以下就是一些规则,它可以保证结构体属性的重叠访问是安全的:
·你访问的是实例的存储属性,而不是计算属性或类的属性
·结构体是本地变量的值,而非全局变量
·结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了