在Swift中,由于其整洁的静态类型系统,很少会同时使用bits(比特)和byetes(字节)。大多数时候,您并不关心底层是什么——您只需要使用类、结构、枚举或基本类型,如Int、Float或String。但迟早,几乎每个Swift开发人员都会遇到这样的情况,即需要亲自动手访问通常隐藏在这些类型背后的原始数据(raw data)。如果你不知道自己在做什么,事情很快就会失去控制……
我们写这篇文章是为了阐明Swift的“黑暗角落”,让你在使用指针和原始字节时感到更安全——最重要的是:我们希望您在访问内存时知道自己在做什么,换句话说:我们希望您在unsafe(不安全的)Swift指针领域中感到安全。
1. The Basics: Memory Layout in Swift
本节是文章其余部分的先决条件。 但是,如果您了解内存管理的基础知识,可以跳过本节,直接跳到Swift中的指针。
从最简单(但仍然准确)的角度来看,内存只是一长串比特(0和1):
01001100010011110101011001000101...
单个bits本身通常没有什么意义,在几乎所有情况下,只有一组bits对我们有意义。通常是8个一组。让我们将这个bit string在内存中分成8-bit组:
01001100 01001111 01010110 01000101 ...
我们称这些8-bit组为一个字节。
现在我们的内存通常是相当大的。我们要处理的是千兆甚至兆兆字节的数据。(1 GB是这些组中的10亿个。) 所以当在内存中存储数据或从内存中读取数据时,我们需要一种方法来标记我们所引用的字节。 最直接的方法就是使用一个递增的整数。我们称这个数字为一个字节的地址。

大多数系统不是按字节(byte-wise)访问存储的数据,而是按字(word-wise)访问:一个字基本上就是多个字节。 一个单词的长度(即每个单词的字节数)取决于平台。 现代平台(如最新的iphone和mac)是64位系统,其中64位决定了每个单词的比特数。因为1字节= 8位,所以在这些系统中,每个单词有64/8 = 8个字节。
所以这是64位系统中更好的内存表示:

最后,我们通常用十六进制数表示地址和存储的字节:

现在是最酷的部分:
字节本身的地址也可以存储在内存中——因此,地址的大小与单词的大小相同:在64位系统中是8个字节。所以实际上,地址都是8个字节:

换句话说:一个地址有一个单词的大小,因此完全适合在内存中。在下面的例子中,存储在address…08引用了这个词的地址…18:

当我们将内存中的一个单词解释为内存中另一个地址的引用时,我们称这个单词为指针(pointer)(因为它指向另一个地址)。
注意:不仅有书面语言,还有计算机平台读“相反的方向”,也就是从右到左。在这些平台上,单词

将按字节顺序反转,看起来像这样:

我们称第一个表示为大端(最有意义的字节在前面),称后一个表示为小端(最没有意义的字节在前面)。
到目前为止,一切顺利吗? 太棒了! 然后让我们采取下一步,看看如何在Swift中使用这些指针!
2. Pointers in Swift
当我开始在Swift中使用指针时,我被大量的指针类型所淹没。 这个API并不容易,尤其是当您是新手的时候。对于我来说,在我正在编写的代码中,找出哪种指针类型或哪种方法是正确的是很困难的。更糟糕的是,所有这些指针方法都是以“不安全(unsafe)”开头的,这让我有点害怕,一开始就不鼓励我使用这些指针。
事实证明,当你以结构化的方式查看Swift指针时,它并没有看起来那么复杂!
2.1. Pointer Types
- Look at unsafe as a simple prefix for all pointers.
是的,有一些其他的指针类型不是以不安全开头的,但是除非您正在做一些非常奇特的事情(比如连接到C / objective -C方法),否则您很可能会使用不安全的指针。换句话说:每个指针*都不安全。unsafe的前缀只是为了提醒你,你需要知道你用指针做什么,你基本上离开Swift舒适的内存管理和类型安全,你几乎“不能做任何错误”(在访问内存方面)。但话虽如此,如果你把前缀放在脑海里会有帮助,这就是我们在这部分要做的。
2.2. How Pointer Types Are Classified
Swift指针的分类依据有3个属性:
- Single or Repeating?
- Typed or Untyped?
- Mutable or Immutable?
因为每个属性都有两个可能的值,所以总共有8种组合,对于每种组合,Swift中都有一个单独的指针类型。让我们一个一个快速地过一遍这些属性。
- Single or Repeating?
指针的第一个属性是它是指向存储中的单个值(例如单个Int值),还是指向具有相同内存属性的多个值(例如[Int]数组)。
- 单值指针简单地称为Pointer(没有任何前缀)。
- 指向相同类型值的“数组”的指针称为BufferPointer。
您可以将Buffer视为具有相同大小内存空间的数组。
- Typed or Untyped?
内存中的字节没有意义,除非你知道如何解释它们。值的类型提供了该信息。这就是第二个指针属性的作用:
- 您可以使用一个知道它所指向字节类型的类型化指针来访问内存。在Swift中,这些指针有一个泛型形参
,表示类型:Pointer 。 - 如果你只对原始字节感兴趣,你也可以不带任何类型信息访问内存。在Swift中,这些指针被称为RawPointer,并且没有泛型参数。
如果将指针的第一个属性和第二个属性结合起来,就会得到四类指针:

- Mutable or Immutable?
最后,这四种指针类型都有一个可变版本和一个不可变版本。当使用可变版本时,您可以写入它所指向的内存。 当您使用不可变版本时,您只能读取内存。
- 在Swift中,可变指针的前缀是Mutable。
- 不可变指针没有这个前缀。
综上所述,在添加了unsafe的前缀后,这就是你在Swift中通常使用的八种指针类型:

3. Working with Pointers in Swift
3.1. Getting a value’s (typed) pointer
对于Swift中的任何值,你都可以使用unsafepointer全局函数访问它的底层指针(如果你需要修改底层内存,也可以使用unsafemutablepointer等效函数)。例如,如果我们有一个整数:
let registrationNumber: Int = 74656
我们可以这样访问它的指针:
withUnsafePointer(to: registrationNumber) { pointer in
// access to the pointer
}
这有点无聊,因为你不能用它做很多事情。对于可变指针,事情变得更有趣了。但是如果我们调用这个函数,Swift编译器就会报错。(这就是为什么它被称为编译器,不是吗?🙈)
withUnsafeMutablePointer(to: registrationNumber) { mutablePointer in
...
}
为什么? 因为我们不能在Swift中改变常量的值,我们在上面用let将registrationNumber定义为常量。 所以让我们来解决这个问题!(我保证,双关语不会比这更糟。😇)
var registrationNumber: Int = 74656
然而,代码仍然不能编译。这是因为当我们想要更改作为参数传递给函数的值时,必须在Swift中将其标记为inout参数。这是通过在值前面加上&来实现的:
withUnsafeMutablePointer(to: ®istrationNumber) { mutablePointer in
// modify the pointer here
}
Now we’re good! 🎉
上面的方法为我们提供了可使用的类型化指针。但大多数时候,我们更感兴趣的是指针的实际字节数。 为了获得这些字节,我们需要内存上的一个不同的视图,一个非类型化视图,换句话说:一个RawPointer或者更准确地说,一个UnsafeRawPointer。
3.2. Accessing a value’s bytes in memory
对于Swift中的任何值,我们都可以使用全局函数unsafebytes访问它的底层原始指针(如果你需要修改底层内存,也可以使用等效的unsafemutablebytes)。这里的命名有点令人困惑,因为我们不是直接访问值的字节,而是访问一个指针。 这些方法应该分别被命名为unsaferawpointerbuffer和withUnsafeMutableRawPointerBuffer,以与指针的名称保持一致,但命名是相当冗长的,Swift团队使用其他名称可能需要有其他一些好的理由。
withUnsafeBytes(of: registrationNumber) { rawPointerBuffer in
// access to the value's raw pointer buffer
}
为什么这个方法给我们一个原始指针缓冲区(raw pointer buffer)而不仅仅是一个原始指针(raw pointer)?
我们在上面说过,原始指针没有类型信息。 但这只是从最顶层看问题的方式。实际上,原始指针有一个类型,这个类型总是UInt8。
UInt8的大小是8位,因此正好是一个字节的大小。因此,对于单个字节来说,它是完美的容器类型。
提示:你可能想要考虑使用一个类型别名到你的Swift项目,所以你有一个表示uint8更有表现力的名字:
typealias Byte = UInt8
这样,你就可以一直写Byte代替UInt8。
现在Swift中任何类型的每个实例都至少占用内存1个字节,但大多数类型都更大。例如,在64位系统中,Int占用8个字节。所以如果withUnsafeBytes函数只给我们一个原始指针而不是一个原始指针缓冲区,我们只会得到一个指向任何类型实例的第一个字节的指针。99%的情况下这不是我们想要的。通常,我们真的想要访问组成值的所有字节,如果它是多个字节(多个UInt8s),我们当然需要一个缓冲区。
最后,如果我们想要修改内存中值的底层字节,我们需要一个可变的原始指针缓冲区:
withUnsafeMutableBytes(of: ®istrationNumber) { mutablePointerBuffer in
// modify the pointer buffer (the bytes) here
}
(注意,我们需要将registrationNumber标记为inout参数,并像之前一样将其设置为var。)
4. Working with Raw Bytes in Swift
那是一大堆理论!现在让我们实践一下,看看如何处理内存中的字节:
好处是,就像数组一样,每个指针缓冲区都是一个Sequence。所以我们可以用for…循环以及所有我们从数组中知道的东西:
withUnsafeBytes(of: registrationNumber) { pointerBuffer in
for byte in pointerBuffer {
print(byte)
}
}
这个函数将registrationNumber的底层字节的值打印为UInt8值:
160
35
1
0
0
0
0
0
你可能会惊讶于零在末尾而不是开头。这是因为我运行这段代码的Mac是一个小端机器,这意味着最不重要的字节排在前面。
记住,注册号是74656。让我们修改这个值的前三个字节…
withUnsafeMutableBytes(of: ®istrationNumber) { mutablePointerBuffer in
mutablePointerBuffer[0] = 165
mutablePointerBuffer[1] = 6
mutablePointerBuffer[2] = 0
}
...并保留剩余的零字节不动。当我们现在打印registrationNumber的值时,我们看到它已经改变了:
print(registrationNumber) // 1701
这就是我们在字节级上操作值的方法。
此时,您已经可以看到指针不安全的一个原因:如果不小心,可能会在索引处越界访问mutablePointerBuffer。例如,下面的代码可能会崩溃——因为registrationNumber是一个Int类型,因此只包含8个字节(索引0-7):
withUnsafeMutableBytes(of: ®istrationNumber) { mutablePointerBuffer in
mutablePointerBuffer[8] = 1 // 💥 crashes
}
你也可以做更多有趣的事情。例如,下面的函数将所有非有效打印ACSII字符的字节设置为0:
withUnsafeMutableBytes(of: ®istrationNumber) { unsafeMutablePointer in
for (index, byte) in unsafeMutablePointer.enumerated() {
if !(32...126).contains(byte) {
unsafeMutablePointer[index] = 0
}
}
}
当然,您应该使用Swift中的一些高级函数来确保值是有效的ASCII码。(我们甚至在这里处理的是一个Int,所以把它当作一个字符串在第一位置并没有真正的意义。) 但是这个函数展示了如何轻松地在字节级上执行操作。
5. Making Computations With Bytes
最后但同样重要的是,这些带有unsafe (Mutable)Bytes的函数也有一个返回值。您可以使用它进行字节级的计算,然后将结果传递给调用作用域中的代码。
例如,我们可以简单地将所有字节相加,得到类似于(非常基本的)校验和的东西:
let checksum = withUnsafeBytes(of: registrationNumber) { pointerBuffer -> UInt8 in
pointerBuffer.reduce(0, +)
}
或者我们可以定义一个函数来检查registrationNumber的所有字节是否都大于零:
let allBytesNonZero = withUnsafeBytes(of: registrationNumber) { pointerBuffer -> Bool in
pointerBuffer.reduce(true) { (intermediateResult, nextByte) -> Bool in
intermediateResult && (nextByte > 0)
}
}
6. Why “withUnsafeBytes” Works With Closures
当我开始在Swift中使用指针时,我的第一个问题是:为什么当我只想获取实例的字节时,Swift会有如此冗长的语法和一个闭包?
也许你已经可以自己回答这个问题了? 闭包有一个定义良好的作用域,您只能访问该作用域内的指针。原因是指针是由Swift的内存管理系统自动管理的,这意味着它们可能在任何时候被修改、释放或失效。在这些withUnsafeBytes方法的闭包中,您可以保证指针是有效的。当程序流离开这个闭包时,这个保证就消失了。
如果你只是想在Swift中获得任何类型的字节的副本(没有修改它们),并且你想在你的程序范围内继续使用这些字节,这里有一个简单的方法来获得它们:
let bytesArray = withUnsafeBytes(of: registrationNumber, Array.init)
bytesArray现在是一个代表字节的UInt8值数组。您可以安全地使用这个数组,因为它是“out of the unsafe territory”。
7. Wrapping It Up
在本文中,您了解了指针是什么,Swift中存在哪些指针类型,以及如何访问任意给定值的指针来处理内存中的单个字节。 网上有一些很棒的文章,如果你想了解更多关于Swift的数据和指针,你可能想看看:
-
swift unboxed: Size, Stride, Alignment by Greg Heo: 在完成本文之后,这是必读的和合乎逻辑的下一步。 以一种非常直观的方式解释Swift中内存管理的非常重要的概念。
-
Swift Pointers Overview: Unsafe, Buffer, Raw and Managed Pointers by Vadim Bulavin: 快速概述Swift中的所有指针类型,包括本文中没有提到的指针类型。
-
Unsafe Swift: Using Pointers and Interacting With C by Brody Eller: 这个教程展示了一些很好的Swift指针应用程序。
-
UnsafePointer:苹果关于UnsafePointer类型的文档中有一些非常好的例子,并提供了进一步的信息。