前言
Objective-C和C语言经常需要使用到指针。Swift中的数据类型由于良好的设计,使其可以和基于指针的C语言API无缝混用。但是语法上有很大的差别。
默认情况下,Swift 是内存安全的,这意味着它禁止我们直接操作内存,并且确保所有的变量在使用前都已经被正确地初始化了。但是,Swift 也提供了我们使用指针直接操作内存的方法,直接操作内存是很危险的行为,很容易就出现错误,因此官方将直接操作内存称为 “unsafe 特性”。
一旦我们开始直接操作内存,一切就得靠我们自己了,因为在这种情况下编译能给我们提供的帮助实在不多。正常情况下,我们在与 C 进行交互,或者我们需要挖掘 Swift 内部实现原理的时候会需要使用到这个特性。
Memory Layout
Swift 提供了 MemoryLayout
来检测特定类型的大小以及内存对齐大小:
MemoryLayout<Int>.size // return 8 (on 64-bit) MemoryLayout<Int>.alignment // return 8 (on 64-bit) MemoryLayout<Int>.stride // return 8 (on 64-bit) MemoryLayout<Int16>.size // return 2 MemoryLayout<Int16>.alignment // return 2 MemoryLayout<Int16>.stride // return 2 MemoryLayout<Bool>.size // return 2 MemoryLayout<Bool>.alignment // return 2 MemoryLayout<Bool>.stride // return 2 MemoryLayout<Float>.size // return 4 MemoryLayout<Float>.size // return 4 MemoryLayout<Float>.alignment // return 4 MemoryLayout<Double>.stride // return 8 MemoryLayout<Double>.alignment // return 8 MemoryLayout<Double>.stride // return 8</div>
MemoryLayout<Type>
是一个用于在编译时计算出特定类型(Type)的 size
, alignment
以及 stride
的泛型类型。返回的数值以字节为单位。例如 Int16
类型的大小为 2 个字节,内存对齐为 2 个字节以及当我们需要连续排列多个 Int16
类型时,每一个 Int16
所需要占用的大小(stride)为 2 个字节。所有基本类型的 stride
都与 size
是一致的。
接下来,看看结构体类型的 MemoryLayout:
struct EmptyStruct {} MemoryLayout<EmptyStruct>.size // returns 0 MemoryLayout<EmptyStruct>.alignment // returns 1 MemoryLayout<EmptyStruct>.stride // returns 1 struct SampleStruct { let number: UInt32 let flag: Bool } MemoryLayout<SampleStruct>.size // returns 5 MemoryLayout<SampleStruct>.alignment // returns 4 MemoryLayout<SampleStruct>.stride // returns 8</div>
空结构体的大小为 0,内存对齐为 1, 表明它可以存在于任何一个内存地址上。有趣的是 stride
为 1,这是因为尽管结构为空,但是当我们使用它创建一个实例的时候,它也必须要有一个唯一的地址。
对于 SampleStruct
,它所占的大小为 5,但是 stride 为 8。这是因为编译需要为其填充空白的边界,使其符合它的 4 字节内存边界对齐。
再来看看类:
class EmptyClass {} MemoryLayout<EmptyClass>.size // returns 8 (on 64-bit) MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit) MemoryLayout<EmptyClass>.stride // returns 8 (on 64-bit) class SampleClass { let number: Int64 = 0 let flag: Bool = false } MemoryLayout<SampleClass>.size // returns 8 (on 64-bit) MemoryLayout<SampleClass>.aligment // returns 8 (on 64-bit) MemoryLayout<SampleClass>.stride // returns 8 (on 64-bit)</div>
由于类都是引用类型,所以它所有的大小都是 8 字节。
关于 MemoryLayout 的更多详细信息可以参考 Mike Ash 的演讲。
指针
一个指针就是对一个内存地址的封装。在 Swift 当中直接操作指针的类型都有一个 “unsafe” 前缀,所以它的指针类型称为 UnsafePointer
。这个前缀似乎看起来很令人恼火,不过这是 Swift 在提醒你,你现在正在跨越雷池,编译器不会对这种操作进行检查,你需要对自己的代码承担全部的责任。
Swift 中包含了一打类型的指针类型,每个类型都有它们的作用和目的,使用适当的指针类型可以防止错误的发生并且更清晰地表达开发者的意图,防止未定义行为的产生。
Swift 的指针类型使用了很清晰的命名,我们可以通过名字知道这是一个什么类型的指针。可变或者不可变,原生(raw)或者有类型的,是否是缓冲(buffer)类型,这三种特性总共组合出了 8 种指针类型。
接下来的几个小节会详细介绍这几种指针类型。
使用原生(Raw)指针
在 Playground 中添加如下代码:
// 1 let count = 2 let stride = MemoryLayout<Int>.stride let alignment = MemoryLayout<Int>.alignment let byteCount = stride * count // 2 do { print("Raw pointers") // 3 let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment) // 4 defer { pointer.deallocate(bytes: byteCount, alignedTo: alignment) } // 5 pointer.storeBytes(of: 42, as: Int.self) pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self) pointer.load(as: Int.self) pointer.advanced(by: stride).load(as: Int.self) // 6 let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount) for (index, byte) in bufferPointer.enumerated() { print("byte \(index): \(byte)") } }</div>
在这个代码段中,我们使用了 Unsafe Swift 指针去存储和读取两个整型数值。
接下来是对这段代码的解释:
1、声明了接下来都会用到的几个常量:
count
表示了我们要存储的整数的个数stride
表示了 Int 类型的 stridealignment
表示了 Int 类型的内存对齐byteCount
表示占用的全部字节数
2、使用 do
来增加一个作用域,让我们可以在接下的示例中复用作用域中的变量名
3、使用 UnsafeMutableRawPointer.allocate
方法来分配所需的字节数。我们使用了 UnsafeMutableRawPointer
,它的名字表明这个指针可以用来读取和存储(改变)原生的字节。
4、使用 defer
来保证内存得到正确地释放。操作指针的时候,所有内存都需要我们手动进行管理。
5、storeBytes
和 load
方法用于存储和读取字节。第二个整型数值的地址通过对 pointer
的地址前进 stride
来得到。因为指针类型是 Strideable
的,我们也可以直接使用指针算术运算 (pointer+stride).storeBytes(of: 6, as: Int.self
)。
6、UnsafeRawBufferPointer
类型以一系列字节的形式来读取内存。这意味着我们可以这些字节进行迭代,对其使用下标,或者使用 filter
,map
以及 reduce
这些很酷的方法。缓冲类型指针使用了原生指针进行初始化。
使用类型指针
我们可以使用类型指针实现跟上面代码一样的功能,并且更简单:
do { print("Typed pointers") let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count) pointer.initialize(to: 0, count: count) defer { pointer.deinitialize(count: count) pointer.deallocate(capacity: count) } pointer.pointee = 42 pointer.advanced(by: 1).pointee = 6 pointer.pointee pointer.advanced(by: 1).pointee let bufferPointer = UnsafeBufferPointer(start: pointer, count: count) for (index, value) in bufferPointer.enumerated() { print("value \(index): \(value)") } }</div>
注意到以下几点不同:
- 我们使用了
UnsafeMutablePointer.allocate
进行内存的分配。指定的泛型参数让 Swif