在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亿个。) 所以当在内存中存储数据或从内存中读取数据时,我们需要一种方法来标记我们所引用的字节。 最直接的方法就是使用一个递增的整数。我们称这个数字为一个字节的地址。

avatar

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

所以这是64位系统中更好的内存表示:

avatar

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

avatar

现在是最酷的部分:

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

avatar

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

avatar

当我们将内存中的一个单词解释为内存中另一个地址的引用时,我们称这个单词为指针(pointer)(因为它指向另一个地址)。


注意:不仅有书面语言,还有计算机平台读“相反的方向”,也就是从右到左。在这些平台上,单词

avatar

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

avatar

我们称第一个表示为大端(最有意义的字节在前面),称后一个表示为小端(最没有意义的字节在前面)。


到目前为止,一切顺利吗? 太棒了! 然后让我们采取下一步,看看如何在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个属性:

  1. Single or Repeating?
  2. Typed or Untyped?
  3. Mutable or Immutable?

因为每个属性都有两个可能的值,所以总共有8种组合,对于每种组合,Swift中都有一个单独的指针类型。让我们一个一个快速地过一遍这些属性。

  1. Single or Repeating?

指针的第一个属性是它是指向存储中的单个值(例如单个Int值),还是指向具有相同内存属性的多个值(例如[Int]数组)。

  • 单值指针简单地称为Pointer(没有任何前缀)。
  • 指向相同类型值的“数组”的指针称为BufferPointer

您可以将Buffer视为具有相同大小内存空间的数组。

  1. Typed or Untyped?

内存中的字节没有意义,除非你知道如何解释它们。值的类型提供了该信息。这就是第二个指针属性的作用:

  • 您可以使用一个知道它所指向字节类型的类型化指针来访问内存。在Swift中,这些指针有一个泛型形参,表示类型:Pointer
  • 如果你只对原始字节感兴趣,你也可以不带任何类型信息访问内存。在Swift中,这些指针被称为RawPointer,并且没有泛型参数。

如果将指针的第一个属性和第二个属性结合起来,就会得到四类指针:

avatar
  1. Mutable or Immutable?

最后,这四种指针类型都有一个可变版本和一个不可变版本。当使用可变版本时,您可以写入它所指向的内存。 当您使用不可变版本时,您只能读取内存。

  • 在Swift中,可变指针的前缀是Mutable。
  • 不可变指针没有这个前缀。

综上所述,在添加了unsafe的前缀后,这就是你在Swift中通常使用的八种指针类型:

avatar

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: &registrationNumber) { mutablePointer in
    // modify the pointer here
}

Now we’re good! 🎉

上面的方法为我们提供了可使用的类型化指针。但大多数时候,我们更感兴趣的是指针的实际字节数。 为了获得这些字节,我们需要内存上的一个不同的视图,一个非类型化视图,换句话说:一个RawPointer或者更准确地说,一个UnsafeRawPointer。

3.2. Accessing a value’s bytes in memory

对于Swift中的任何值,我们都可以使用全局函数unsafebytes访问它的底层原始指针(如果你需要修改底层内存,也可以使用等效的unsafemutablebytes)。这里的命名有点令人困惑,因为我们不是直接访问值的字节,而是访问一个指针。 这些方法应该分别被命名为unsaferawpointerbufferwithUnsafeMutableRawPointerBuffer,以与指针的名称保持一致,但命名是相当冗长的,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: &registrationNumber) { 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: &registrationNumber) { mutablePointerBuffer in
    mutablePointerBuffer[0] = 165
    mutablePointerBuffer[1] = 6
    mutablePointerBuffer[2] = 0
}

...并保留剩余的零字节不动。当我们现在打印registrationNumber的值时,我们看到它已经改变了:

print(registrationNumber) // 1701

这就是我们在字节级上操作值的方法。

此时,您已经可以看到指针不安全的一个原因:如果不小心,可能会在索引处越界访问mutablePointerBuffer。例如,下面的代码可能会崩溃——因为registrationNumber是一个Int类型,因此只包含8个字节(索引0-7):

withUnsafeMutableBytes(of: &registrationNumber) { mutablePointerBuffer in
    mutablePointerBuffer[8] = 1 // 💥 crashes
}   

你也可以做更多有趣的事情。例如,下面的函数将所有非有效打印ACSII字符的字节设置为0:

withUnsafeMutableBytes(of: &registrationNumber) { 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的数据和指针,你可能想看看: