iOS Swift No.25 - 内存安全1

第二十五章 内存安全

默认情况下,swift语言会阻止我们代码中的一些不安全行为,举个例子,swift确保变量在使用前一定要先构造好,内存在某个变量被释放之后并不能读取该变量,并且会检查超范围的数组的索引值,预防发生内存读取数组中超范围的索引值。

Swift同样会确保多次读取内存中相同的区域而不会发生冲突,通过请求修改内存中的一个位置而达到拥有对内存的独占访问权。因为swift是自动管理内存的。大多数情况下我们不需要思考访问内存,然而,我们需要理解什么时候会发生冲突时非常重要的,所以我们在写代码的时候要避免冲突访问内存,如果我们的代码中包含某个冲突访问,那么会在代码编译的时候出现报错。

1. Understanding Conflicting Access to Memory (理解冲突访问内存)

访问内存发生在我们的代码中,当我们为变量设置某个值,或着给函数传入某个实参,举个例子,下面这段代码就同时含有读取访问和写入访问。

// 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中,我们可以跨越代码总的好几行来修改一个值,使它有可能在自身的修改中尝试访问某一个值。In Swift, there are ways to modify a value that span several lines of code, making it possible to attempt to access a value in the middle of its own modification.

举个例子我们可以考虑如何在纸上写如何更新预算,更新预算一共分为两个步骤,第一步添加物品的名字和价格,然后更改总价用于反应当前在列表中的物品数量。在更新之前和之后,你可以阅读预算中的任何信息和正确的结果。
在这里插入图片描述

在预算列表上添加物品的时候,它是临时的,无效的状态,是因为我们并没有更新添加新的物品之后的总金额。在添加物品的过程中,阅读总金额是一个不正确的信息。

这个例子同样也像我们展示了一个挑战,就是当我们在修复内存访问冲突时的一个挑战。有些时候会有一些多个方法来修复冲突会产生多个不同的结果。并不时很明显的告诉我们哪一个结果时正确的。所以在这个例子中取决于我们是否想要原总金额或者是更新之后的总金额。5元或320元都可能是正确的结果。在解决这个冲突之前,我们要决定想要的最终结果是什么。
在这里插入图片描述

1.1 Characteristics of Memory Access (内存访问的特性)

在冲突访问的上下文关系中需要考虑内存访问的三个特性:访问是读取还是写入,访问持续时间,访问内存中的位置。特别需要注意,如果冲突发生,那就是因为两次访问都遇到了下面的所有情况。

  • At least one is a write access. (至少有一个是写入访问)
  • They access the same location in memory.(访问内存相同的位置)
  • Their durations overlap.(访问持续的时间重叠了)

读取和写入访问的不同是很明显的:写入访问会改变内存中的位置,而读取访问不会。内存中的位置指代的就是被访问的对象,举个例子,一个变量,常量,或属性。内存读取的持续时间要么是即时的(instantaneous)访问要么是长期的(long-term)访问。

如果说某个访问是即时的,那就意味着该访问在开始之后和结束之前都不能运行其他代码。这就是持续时间内访问重叠的意思。一个长期访问可以于即时访问和其他长期访问重叠。

重叠访问主要出现在使用了输入输出型参数的函数或方法中,或者是结构体的某个可变的方法中,这些特殊的swift代码使用长期访问会在下个片段有详细介绍的。

2 Conflicting Access to In-Out Parameters (输入输出型参数中的冲突访问)

函数具有对所有输入输出型参数的长期写入访问权限。为输入输出型参数的写入访问开始于非输入输出型参数计算完毕之后且持续至整个函数的调用,如果有很多个输入输出型参数,写入访问开始的顺序和多个参数出现的顺序是相同的。

长期写入访问的一个重要性就是在于我们不能读取以输入输出型参数传入的原变量值。即便是范围规则和访问控制都可以允许它-任何对原变量值的访问都会创建一个冲突。举个例子。

var stepSize = 1
func increment(_ number: inout Int) {
    number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize

在上面的代码中,stepSize是一个全局变量,一般情况下可以在increment(_:)方法中访问该变量的。然而,对stepSize的读取访问和对number的写入访问重叠了。见下图列,number和stepSize的访问指向了内存中的同一个位置。读取和写入访问指向的是同一个位置,这样就会产生冲突。
在这里插入图片描述
有一种方式来化解这个冲突就是在重新复制一个stepSize的副本。

// 创建一个stepSize的副本copyOfStepSize.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)
// 更新原来的stepSize.也就是给原来的stepSize赋copyOfStepSize的值
stepSize = copyOfStepSize

我们在调用increment(_:)之前就已经创建好了stepSize的副本。所以就很清楚了,copyOfStepSize的值是以当前stepSize的值来增加的。所以写入访问是在读取访问结束之后进行的,先读取原来的stepSize,在增加stepSize的副本也就是copyOfStepSize,一次读取一次写入,最后才是将副本copyOfStepSize写入原stepSize内,这样就没有了冲突,读取和写入访问都没有在同一时间同一个内存位置上发生。

另一个给输入输出型参数的长期写入访问的重要性就是,为相同函数的多个输入输出型参数传递单个变量作为参数的,会产生冲突。Another consequence of long-term write access to in-out parameters is that passing a single variable as the argument for multiple in-out parameters of the same function produces a conflict.

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(_:_:)函数修改两个参数,在这些值之间均匀划分总值。将playerOneScore和playerTwoScore作为参数来调用该函数balance(_:_:),不会产生冲突。两个写入访问时间重叠了。但是它们却访问的是内存中的不同位置。相比起来,将playerOneScore作为两个参数值来传递给balance(_:_:)函数肯定会产生冲突。因为在同意时间尝试在执行两个写入访问给内存的相同位置。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45026183/article/details/107888931
今日推荐