Swift 内存安全详解

默认情况下,Swift可以防止代码中出现不安全行为。例如,Swift确保变量在使用之前被初始化,内存在被释放后不被访问,数组索引被检查是否越界。
Swift还确保对同一内存区域的多次访问不会发生冲突,这是因为只有需要修改内存中某个位置的代码才拥有对该内存的访问权限。因为Swift是自动管理内存的,通常情况下根本不需要考虑访问内存。但是,了解潜在冲突可能发生在哪里是很重要的,这样可以避免编写访问内存的冲突代码。如果代码中确实包含冲突,则在编译或运行时发生错误。

一、内存访问冲突

当执行诸如设置变量值或向函数传递参数等操作时,就会在代码中发生对内存的访问。例如,以下代码包含读访问和写访问:

// A write access to the memory where one is stored.
var one = 1

// A read access from the memory where one is stored.
print("We're number \(one)!")

当代码的不同部分试图同时访问内存中的相同位置时,可能会发生内存访问冲突。同时多次访问内存中的某个位置会产生不可预测或不一致的行为。在Swift中,有一些方法可以修改一个跨越多行代码的值,从而可以尝试在自己的修改过程中访问一个值。

通过思考如何更新写在纸上的预算,你可以看到类似的问题。更新预算需要两个步骤:首先添加商品的名称和价格,然后更改总额以反映当前清单上的商品。在更新之前和之后,您可以从预算中读取任何信息并得到正确的答案,如下图所示。
memory_shopping_2x.png
当你向预算中添加商品时,它处于临时的、无效的状态,因为总金额尚未更新来反映新添加的商品。在添加一个商品的过程中,读取总金额会获取错误的信息。

这个例子还演示了在修复内存的访问冲突时可能遇到的一个挑战:有时有多种方法可以解决冲突,同时也产生不同的答案,而且无法确定哪个答案是正确的。在本例中,根据您想要原始的总额还是更新后的总额,5美元或320美元可能是正确的答案。在修复冲突访问之前,你必须确定要执行的操作。

注意:如果你编写过并发或多线程代码,则对内存的冲突访问可能是一个熟悉的问题。但是,此处讨论的冲突访问可能发生在单个线程上,并且不涉及并发或多线程代码。
如果在单线程中存在内存访问冲突,Swift会保证在编译时或运行时都会收到错误。对于多线程代码,请使用Thread Sanitizer帮助检测跨线程的冲突访问。

二、内存访问的特征

在冲突访问的上下文中,需要考虑内存访问的三个特征:访问是读还是写,访问的持续时间,以及在内存中的访问位置。具体来说,如果您有两个满足以下所有条件的访问,就会发生冲突:

  • 至少有一个是写访问。
  • 它们访问内存中的相同位置。
  • 它们的时间重叠。
    读访问和写访问之间的区别通常很明显:写访问改变内存中的位置,但读访问不会。内存中的位置是指正在访问的内容 - 例如,变量,常量或属性。内存访问的持续时间可以是瞬时的,也可以是长期的。
    如果在访问开始之后但在访问结束之前不能运行其他代码,则访问是瞬时的。从本质上讲,两次瞬时访问不可能同时发生。大多数内存访问是瞬时的。例如,下面代码清单中的所有读写访问都是瞬时的:
func oneMore(than number: Int) -> Int {
    return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"

但是,有几种访问内存的方法,称为长期访问,可以跨越其他代码的执行。瞬时访问和长期访问的区别在于,其他代码可以在长期访问开始后结束之前运行,这称为重叠。长期访问可以与其他长期访问和瞬时访问重叠。
重叠访问主要出现在在函数和方法中使用in-out参数的代码中,或者是结构体的可变方法中。使用长期访问的特定Swift代码类型将在下面的部分中讨论。

三、对In-Out参数的访问冲突

函数拥有对其所有in-out参数的长期写访问权。一个in-out参数的写访问在所有非in-out参数被计算之后开始,并持续到整个函数调用期间。如果有多个in-out参数,则写访问的开始顺序与参数出现的顺序相同。
这种长期写访问的一个后果是,您不能访问作为in-out传递的原始变量,即使范围规则和访问控制允许这样做——任何对原始变量的访问都会产生冲突。例如:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

在上面的代码中,stepSize是一个全局变量,通常可以从increment(_:)访问它。但是,对stepSize的读访问与对number的写访问重叠。如下图所示,numberstepSize都指向内存中的同一个位置。读和写访问引用相同的内存,它们重叠,产生冲突。

memory_increment_2x.png

解决这个冲突的一种方法是显式复制stepSize:

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2

当在调用increment(_:)之前复制一个stepSize时,很明显copyOfStepSize的值是由当前stepSize递增的。读访问在写访问开始之前结束,所以没有冲突。
对in-out参数进行长期写访问的另一个后果是,将单个变量作为同一个函数的多个in-out参数的参数传递会产生冲突。例如:

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore

上面balance(_:_:)函数修改了它的两个参数,使它们之间的值相等。用playerOneScoreplayerTwoScore调用它不会产生冲突——有两个写访问在时间上重叠,但它们访问的是不同的内存位置。相反,将playerOneScore作为两个参数的值传递会产生冲突,因为它试图同时执行对内存中相同位置的两次写访问。

注意:由于操作符是函数,它们也可以长期访问它们的in-out参数。
因为运算符是函数,所以它们也可以长期访问其in-out参数。例如,如果balance(_:_:)是一个名为<^>的运算符函数,则写入playerOneScore <^> playerOneScore将导致与balance(&playerOneScore, &playerOneScore)相同的冲突。

四、方法中self的访问冲突

结构体上的可变方法在方法调用期间对self具有写访问权。例如,考虑这样一个游戏,每个玩家都有一个生命值(在受到伤害时减少)和一个能量值(在使用特殊技能时减少)。

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

在上面的restoreHealth()方法中,对self的写访问从方法的开头开始,一直持续到方法返回为止。在本例中,restoreHealth()中没有其他代码可以重叠访问Player实例的属性。下面的 shareHealth(with:)方法将另一个Player实例作为一个in-out参数,创建了重叠访问的可能性。

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)  // OK

在上面的例子中,调用Oscar玩家的shareHealth(with:)方法与Maria 玩家共享生命值不会引起冲突。在方法调用期间有对oscar的写访问,因为oscar是可变方法中的self值,在相同的时间内也有对maria的写访问,因为maria是作为 in-out参数传递的。如下图所示,它们访问内存中的不同位置。尽管两个写访问在时间上是重叠的,但它们并不冲突。

memory_share_health_maria_2x.png

但是,如果将oscar作为参数传递给shareHealth(with:),则存在冲突:

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar

在该方法的持续时间内,可变方法需要对self的写访问。而在相同的持续时间内,in-out参数需要对teammate的写访问。在方法中,selfteamate引用内存中的相同位置—如下图所示。这两个写访问引用相同的内存,它们重叠,产生冲突。

memory_share_health_oscar_2x.png

五、属性的访问冲突

结构体、元组和枚举等类型由单个组成值组成,例如结构体的属性或元组的元素。由于这些都是值类型,因此改变值的任何部分会改变整个值,这意味着对其中一个属性的读或写访问需要对整个值进行读或写访问。例如,对元组元素的重叠写访问会产生冲突:

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation

在上面的例子中,调用元组元素上的balance(_:_:)会产生冲突,因为对playerInformation的写访问有重叠。playerInformation.healthplayerInformation.energy作为in-out参数传递,这意味着balance(_:_:)在函数调用期间需要对它们进行写访问。在这两种情况下,对tuple元素的写访问都需要对整个tuple进行写访问。这意味着有两个对playerInformation的写访问,它们的持续时间重叠,导致冲突。
下面的代码显示,对存储在全局变量中的结构体属性的重叠写访问,会出现相同的错误。

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // Error

实际上,大多数对结构体属性的访问都可以安全地重叠。例如,如果上例中的变量holly更改为局部变量而不是全局变量,则编译器可以证明对结构体存储属性的重叠访问是安全的:

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // OK
}

在上面的例子中,oscarhealthenergy 作为两个输入参数传递给balance(_:_:)。编译器可以证明保留了内存安全性,因为这两个存储属性不以任何方式交互。

反对重叠访问结构体属性的限制并不总是保证内存安全的必要条件。内存安全是理想的保证,但唯一的访问是比内存安全更严格的要求 - 这意味着一些代码可以维护内存安全,即使它违反了对内存的唯一访问权限。如果编译器可以证明对内存的非单独访问仍然是安全的,那么Swift允许这种内存安全的代码。具体而言,如果满足以下条件,则可以证明对结构属性的重叠访问是安全的:

  • 只访问实例的存储属性,而不是计算属性或类属性。
  • 结构体是局部变量的值,而不是全局变量。
  • 该结构体要么不被任何闭包捕获,要么仅由非逃离闭包捕获。
    如果编译器无法证明访问是安全的,则不允许访问。

六、其他专题模块

Swift 4.2 基础专题详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,065评论 1 32
  • 不知道大家有没有这样的经历... 平时已经不怎么联系的朋友,突然发来一条信息:客服正在帮我清理僵尸粉,感谢微信有你...
    稍磨制就阅读 14,497评论 0 0
  • 在雾汽濛濛的早上 知更鸟不要迷失方向 日复一日的习惯 即使在迟来的朝阳 知更鸟 不知道 没有什么需要你去阻挡 没...
    肖魁之阅读 585评论 0 2
  • 2017年12月10日,星期日,伍哥读报时间: 1、【京津冀公立医院耗材联合采购】12月7日,北京市卫生计生委印发...
    邢五阅读 104评论 0 0
  • 1996到2016,今年我20岁,读大三。 打开朋友圈,经常看着和我年龄相仿或是稍大的已婚女性5-8连发式刷屏,内...
    童圆圆O_O阅读 337评论 1 2