Swift内存模型与指针的使用

一、前言

我们在编写Objective-C和C代码时经常会用到指针,切换到Swift语言中来,虽然其可以无缝使用C语言指针,但是在语法上与OC和C还是有很大区别的。

Swift本身是内存安全的,只要确保所有变量在使用前都被正确地初始化,我们不必担心内存问题,因此,Apple官方不建议开发者直接操作内存,但是Swift还是为开发者提供了使用指针直接操作内存的方法,Swift中所有的指针类型都带有“Unsafe”前缀,就是为了提示开发者使用指针的行为是危险且不安全的,要慎重。

先来看看Swift中的指针类型:

C Swift 说明
const Type * UnsafePointer 指针可变,指针指向的内存值不可变
Type * UnsafeMutablePointer 指针可变,指针指向的内存值可变
const void * UnsafeRawPointer 指针可变,指针指向的内存区域类型不确定,且内存值不可变
void * UnsafeMutableRawPointer 指针可变,指针指向的内存区域类型不确定,且内存值可变
StructType * OpaquePointer C中的一些自定义类型,Swift中无相应的类型,如:sqlite3_stmt结构体
ClassType const UnsafePointer 指向指针的指针,指针不可变,指针指向的类可变
ClassType __strong UnsafeMutablePointer 指向指针的指针,指针可变,指针指向的类可变
int8_t a[] var x:[Int8] -> UnsafeBufferPointer 数组指针,指向数组一个元素的地址

二、MemoryLayout

提到指针就不得不说一下内存模型。Swift的内存分配与C/C++/Objective-C/Java等类似:

  • Stack: 存储值类型变量(如int、float、struct等),函数调用栈,存储引用类型的临时变量指针
  • Heap: 存储引用类型的实例,比如类的对象

内存对齐:出于对寻址速度、原子操作以及简化CPU和Memory之间接口设计等方面的考虑,现代计算机系统对值类型的合法地址做了一些限制,要求某种数据类型的对象的地址必须是K的整数倍(K通常是2、4、8)。

因此对齐的原则是: 任何K字节的基本对象的地址必须是K的整数倍

Tip: 在声明类或者结构体的成员变量时,可以将占用空间大的变量写在前面,将占用空间小的变量写在后面,以达到减少内存占用的目的。

MemoryLayout是Swift中用来计算数据类型占用内存空间大小的工具,它有三个比较常用的Int类型的属性:

  • size/size(ofValue: T): T类型的实例占用连续内存字节的大小
  • alignment/alignment(ofValue: T): 数据类型T的内存对齐原则,在64bit系统下,最大的内存对齐原则是8Byte
  • stride/stride(ofValue: T): 由于内存对齐的原因,T类型的实例实际消耗的内存空间stride可能比其size大,浪费的内存空间即:stride - size

其简单用法如下:

1
2
3
4
5
6
var count: Int = 0

MemoryLayout<Int>.size // 8
MemoryLayout.size(ofValue: count) // 8
MemoryLayout<Int>.alignment // 8
MemoryLayout<Int>.stride // 8

我们看下Swift中基本数据类型的MemoryLayout三个属性的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Test: NSObject {}
var count: Int = 0

//MARK:- 值类型
MemoryLayout<Int>.size // 8
MemoryLayout<Int>.alignment // 8
MemoryLayout<Int>.stride // 8

MemoryLayout<Double>.size // 8
MemoryLayout<Double>.alignment // 8
MemoryLayout<Double>.stride // 8

MemoryLayout<Float>.size // 4
MemoryLayout<Float>.alignment // 4
MemoryLayout<Float>.stride // 4

MemoryLayout<UInt>.size // 8
MemoryLayout<UInt>.alignment // 8
MemoryLayout<UInt>.stride // 8

MemoryLayout<String>.size // 16
MemoryLayout<String>.alignment // 8
MemoryLayout<String>.stride // 16

//MARK: - 引用类型
MemoryLayout<Test>.size // 8
MemoryLayout<Test>.alignment // 8
MemoryLayout<Test>.stride // 8

//MARK: - 指针类型
MemoryLayout<UnsafePointer<Test>>.size // 8
MemoryLayout<UnsafePointer<Test>>.alignment // 8
MemoryLayout<UnsafePointer<Test>>.stride // 8

MemoryLayout<UnsafeBufferPointer<Test>>.size // 16
MemoryLayout<UnsafeBufferPointer<Test>>.alignment // 8
MemoryLayout<UnsafeBufferPointer<Test>>.stride // 16

三、Swift指针

3.1、使用类型指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//MARK: - 使用类型指针
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
ptr.initialize(to: 1) // 赋值

ptr // 0x6000022c00c0
ptr.pointee // 1

ptr.pointee = 100
ptr.pointee // 100

ptr.advanced(by: 1).pointee // 指针向后移动8Bytes,其指向的内存内容不确定

// 使用完不要忘记销毁
ptr.deallocate()
3.2、使用原生指针
1
2
3
4
5
6
7
8
9
10
11
//MARK: - 使用原生指针

let rawPtr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 8)
rawPtr.storeBytes(of: "hello world", as: String.self)
rawPtr.load(as: String.self) // hello world

let bufferRawPtr = UnsafeMutableRawBufferPointer(start: rawPtr, count: 16)
// UnsafeRawBufferPointer类型以一系列字节的形式来读取内存
for (index, byte) in bufferRawPtr.enumerated() {
print("byte \(index): \(byte)")
}
3.3、获取实例的bytes

在Objective-C中我们经常遇到函数接受bytes指针的参数,那么Swift中如何获取bytes指针呢?

1
2
3
4
5
6
7
8
var str = "hello world."
withUnsafeBytes(of: str) { bytes in
for byte in bytes {
print(byte)
}
// 注意:禁止返回指针,如果指针超出withUnsafeBytes作用域,可能会导致意想不到的结果,其他withUnsafeXXX方法同样禁止返回指针
// return bytes
}
3.4、指针类型转换

我们操作C函数的时候,经常会遇到需要转换指针类型的情况,比如将指向结构体的指针转换为指向其它不同结构体的指针,这种操作在C语言中是很简单但也十分危险的。而由于Swift指针在创建时即明确了其类型,这就意味着一个UnsafePointer类型的指针不能用在需要UnsafePointer的地方,比如:sqlite3_column_text返回值不能用在String(utf8String: )方法中,这个时候就需要用到指针类型转换了:

1
2
3
4
5
6
7
8
9

let intPtr = UnsafeMutablePointer<Int8>.allocate(capacity: 1)

intPtr.pointee = -1
intPtr.pointee // -1

let _ = intPtr.withMemoryRebound(to: UInt8.self, capacity: 1) { ptr in
ptr.pointee // 255
}

四、实践——通过指针修改Struct类型实例的属性的值

先直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  struct TestStruct {
var a: Int8 = 7 // 1 bytes
private var b: Int = 6 // 8 bytes
var c: String = "hello" // 16 bytes
var d: Int? // 9 bytes
}

var testStruct = TestStruct()
let rawPtr = withUnsafeMutablePointer(to: &testStruct) {UnsafeMutableRawPointer(mutating: $0)}
let b = rawPtr.load(fromByteOffset: 8, as: Int.self)
let bPtr = rawPtr.advanced(by: 8).assumingMemoryBound(to: Int.self)
bPtr.pointee // 6
bPtr.initialize(to: 10)
bPtr.pointee // 10

代码分析:
rawPtr指针是一个void *类型的指针,指向testStruct实例所在内存的第一个字节,我们可以通过移动指针获取testStructprivate属性b
属性aInt8类型,占用1个字节,但是由于内存对齐的原因,其所占的内存空间为8个字节,因此需要将rawPtr向后移动8个字节才能获取到属性b的起始地址:rawPtr.advanced(by: 8).assumingMemoryBound(to: Int.self)

assumingMemoryBound(to:)

Returns a typed pointer to the memory referenced by this pointer, assuming that the memory is already bound to the specified type.

因此,将rawPtr向后移动8bytes后通过assumingMemoryBound(to: Int.self)方法,可以得到指向属性b的指针bPtr。之后通过initialize(to: 10)方法重新初始化属性b的内存区域,为其重新赋值。