首页 Swift内存安全
文章
取消

Swift内存安全

Swift 是一门安全的、快速的编程语言,其中之一的原因是它在设计上考虑了内存安全。在这篇博客中,我们将深入探讨 Swift 的内存安全机制。

引用和值类型

Swift 中的类型可以被划分为两种:引用类型和值类型。值类型包括结构体(struct)和枚举(enum),而引用类型则包括类(class)。这两种类型不同的一个主要区别就是它们对内存的使用方式。

值类型被赋值给变量或常量时,Swift 会在内存中为这个变量或常量创建一份独立的副本。也就是说,在复制过程中,原始值和复制后的值是完全独立的,彼此之间没有任何联系。例如:

1
2
3
4
5
6
7
8
9
10
11
12
struct Point {
    var x: Int
    var y: Int
}

var point1 = Point(x: 1, y: 2)
var point2 = point1

point1.x = 3

print(point1.x) // 输出 3
print(point2.x) // 输出 1

在这个例子中,当我们将 point1 赋值给 point2 时,Swift 会在内存中为 point2 创建一份独立的副本。所以在修改 point1 中的 x 属性时,point2 并没有受到影响。

引用类型的对象则不同,它们在赋值时只是将指向对象的引用复制了一份。也就是说,多个引用可能指向同一个对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

var person1 = Person(name: "张三")
var person2 = person1

person1.name = "李四"

print(person1.name) // 输出 "李四"
print(person2.name) // 输出 "李四"

在这个例子中,当我们将 person1 赋值给 person2 时,它们实际上是指向同一个 Person 对象的引用。所以当我们修改 person1 中的 name 属性时,person2 中的 name 属性也会被修改。

内存访问冲突

在 Swift 中,有一些情况下可能会出现多个访问同一块内存的情况。这种情况被称为内存访问冲突。内存访问冲突可能导致程序崩溃或者产生意外的行为,因此需要避免出现内存访问冲突。

In-Out 参数

函数的形参默认情况下是常量,如果需要修改传入参数的值,可以将参数声明为 inout,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
func swapInts(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

var a = 1
var b = 2

swapInts(&a, &b)

print(a) // 输出 2
print(b) // 输出 1

在调用 swapInts 函数时,我们需要将参数前加上 & 符号,表示这是一个 inout 参数。如果多个参数同时被声明为 inout,并且它们指向同一块内存,那么就会产生内存访问冲突。

访问数组元素

当我们使用下标访问数组元素时,也有可能会产生内存访问冲突。例如:

1
2
3
4
5
var numbers = [1, 2, 3]

let first = numbers[0]
numbers[0] = numbers[1]
numbers[1] = first

在这个例子中,我们通过交换数组中第一个元素和第二个元素的值来实现了数组元素的交换。但是,在修改数组元素时,我们同时访问了两个数组元素,并且这两个元素指向同一块内存,因此可能会产生内存访问冲突。

为了避免这种情况,Swift 提供了 swapAt(_:_:) 方法,可以用于交换数组中两个元素的位置:

1
2
3
4
5
var numbers = [1, 2, 3]

numbers.swapAt(0, 1)

print(numbers) // 输出 [2, 1, 3]

属性访问冲突

当多个属性指向同一块内存时,也可能会发生属性访问冲突。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

var person = Person(name: "张三")

let name = person.name
person.name = "李四"

print(name) // 输出 "张三"

在这个例子中,我们先将 personname 属性赋值给常量 name,然后再修改 personname 属性。由于 nameperson.name 指向同一块内存,因此可能会产生内存访问冲突。

为了避免这种情况,我们可以使用 Swift 中的 withUnsafeMutablePointer(to:) 方法来获取属性的指针,然后访问指针所指向的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

var person = Person(name: "张三")

let name = withUnsafeMutablePointer(to: &person.name) {
    $0.pointee
}
person.name = "李四"

print(name) // 输出 "张三"

自动引用计数

在编写使用引用类型的代码时,必须考虑到内存管理问题。如果一个对象没有被任何变量或常量引用,那么它就成为了“垃圾”,占用了系统的内存空间。为了解决这个问题,Swift 引入了自动引用计数(ARC)机制。

ARC 会自动跟踪对象的引用数量,当一个对象没有任何引用时,ARC 就会自动释放这个对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 已释放")
    }
}

var person1: Person? = Person(name: "张三")
var person2 = person1

person1 = nil
person2 = nil

// 输出 "张三 已释放"

在这个例子中,我们创建了一个 Person 对象,并将它赋值给变量 person1,然后将 person1 赋值给 person2。当我们将 person1person2 都设置为 nil 后,这个 Person 对象就没有任何引用了,ARC 就会自动释放它。

非托管内存分配

除了自动引用计数外,Swift 还提供了一些手动管理内存的方式。其中最常用的是非托管内存分配。在非托管内存分配中,我们手动分配内存,并手动释放内存,例如:

1
2
3
4
5
let count = 10
let bufferSize = MemoryLayout<Int>.stride * count
let buffer = UnsafeMutableRawPointer.allocate(byteCount: bufferSize, alignment: MemoryLayout<Int>.alignment)
buffer.storeBytes(of: 42, toByteOffset: 0, as: Int.self)
buffer.deallocate()

在这个例子中,我们首先计算需要分配的内存大小,然后调用 UnsafeMutableRawPointer.allocate(byteCount:alignment:) 方法分配内存。接着,我们使用 buffer.storeBytes(of:toByteOffset:as:) 方法向内存中写入数据,最后调用 buffer.deallocate() 方法释放内存。

需要注意的是,在使用非托管内存分配时,我们必须手动管理内存的生命周期,如果没有正确地释放内存,就可能会导致内存泄漏或者悬垂指针等问题。

内存安全规则

为了保证 Swift 代码的内存安全,Swift 标准库定义了一些内存安全规则。遵守这些规则可以有效地避免内存访问冲突等问题。

访问不重叠的内存

访问不重叠的内存是一种安全的内存访问方式。当多个访问指向不同的内存位置时,它们不会产生冲突。例如:

1
2
3
4
5
6
7
8
9
10
var a = 1
var b = 2

withUnsafeMutablePointer(to: &a) { ptrA in
    withUnsafeMutablePointer(to: &b) { ptrB in
        ptrA.pointee += ptrB.pointee
    }
}

print(a) // 输出 3

在这个例子中,我们使用 withUnsafeMutablePointer(to:) 方法获取变量 ab 的指针,并在指针上进行操作,由于两个指针指向不同的内存位置,因此不会产生内存访问冲突。

对于 inout 参数和属性的访问要独占

对于 inout 参数和属性的访问,必须保证独占访问。也就是说,在访问期间,不能有其他访问操作同时发生。例如:

1
2
3
4
5
6
7
8
9
10
11
var numbers = [1, 2, 3]

func doubleInPlace(_ array: inout [Int]) {
    for i in 0..<array.count {
        array[i] *= 2
    }
}

doubleInPlace(&numbers)

print(numbers) // 输出 [2, 4, 6]

在这个例子中,我们定义了一个函数 doubleInPlace,它接受一个 inout 的数组参数,并将数组中的每个元素乘以 2。在调用这个函数时,必须保证没有其他访问操作同时发生,否则就会产生内存访问冲突。

避免跨越长时间的生命周期访问对象

如果一个对象的生命周期很长,那么在访问它的过程中,可能会有许多其他的访问操作同时发生。为了避免内存访问冲突,我们应该尽量缩小对象的生命周期,让访问操作尽可能短暂。例如:

1
2
3
4
5
6
7
8
9
10
func printName(_ person: Person) {
    let name = person.name
    print(name)
}

var person = Person(name: "张三")

printName(person)

person.name = "李四"

在这个例子中,我们定义了一个函数 printName,它接受一个 Person 对象作为参数,并输出这个对象的 name 属性。在调用这个函数时,我们将 person 作为参数传递进去,函数执行完毕后,person 的生命周期就结束了。由于访问操作的时间很短,因此不会产生内存访问冲突。

本文由作者按照 CC BY 4.0 进行授权

Swift中的自动引用计数

Swift访问控制