默认情况下,Swift 是内存安全的。其防止开发者直接访问内存,并确保开发者在使用其之前已经完成了初始化。但这只是“默认情况下”,开发中还可以使用 Unsafe Swift,直接通过指针访问内存。
Unsafe 不是可能无法运行的危险代码,它指的是需要格外小心的代码,这些代码限制了编译器的保护。如果开发者需要与 C 等不安全的语言进行互操作,或者需要获得额外的运行时性能,再者是想探索 Swift 的内部结构,这些功能非常有用。
我们将了解 Swift 的 Unsafe 特性,学习如何使用指针并直接与内存进行交互。
探索内存布局
基本数据类型的内存布局
Unsafe Swift 直接与内存一起工作。我们可以将内存想象成一堆的盒子,这些盒子可能有数十亿个,每个盒子都包含一个数字。
每个盒子都有一个唯一的内存地址(Memory Address ) 。最小的可寻址存储单元是一个字节(Byte) ,通常由八位(Bit) 组成。
一个八位的字节可以存储 0-255 的值。处理器通常可以有效地访问超过一个字节的内存字(Word) 。
在 64 位系统上,一个字是 8 个字节即 64 位。我们可以使用 MemoryLayout 得到原生 Swift 类型的 size 和 alignment:
import Foundation
MemoryLayout<Int>.size // returns 8 (on 64-bit)
MemoryLayout<Int>.alignment // returns 8 (on 64-bit)
MemoryLayout<Int>.stride // returns 8 (on 64-bit)
MemoryLayout<Int16>.size // returns 2
MemoryLayout<Int16>.alignment // returns 2
MemoryLayout<Int16>.stride // returns 2
MemoryLayout<Bool>.size // returns 1
MemoryLayout<Bool>.alignment // returns 1
MemoryLayout<Bool>.stride // returns 1
MemoryLayout<Float>.size // returns 4
MemoryLayout<Float>.alignment // returns 4
MemoryLayout<Float>.stride // returns 4
MemoryLayout<Double>.size // returns 8
MemoryLayout<Double>.alignment // returns 8
MemoryLayout<Double>.stride // returns 8
MemoryLayout<Type> 是编译时评估的泛型类型。它确定每个指定 Type的 size、 alignment 和stride,并返回一个以字节为单位的数字。
举一个例子,一个 Int16 的 size 是两个字节,并且 alignment 也是两个字节。这意味着它必须从偶数地址开始——即可以被 2 整除的地址。例如,在地址 100 处分配一个 Int16 是合法的,但在 101 处则不合法——违反了所要求的对齐方式。当我们将一堆 Int16 打包时,它们以 stride 为间隔。对于这些基本类型,size 与 stride 相同。
结构体和类的内存布局
我们开看下结构体的内存布局:
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
空结构体的 size 为零。因为 alignment 是 1,并且所有数字都可以被 1 整除,所以它可以存在于任何地址。空结构体的 stride 为什么是 1 ?这是因为每个空结构体即使它的 size 为零,都必须具有唯一的内存地址。
因为 SampleStruct 的 size 是 5 ,而 alignment 要求是它位于 4 字节边界上,鉴于此 Swift 就以 8 个字节的 stride 打包。
接着我们查看下类的内存布局有何不同:
class EmptyClass {}
MemoryLayout<EmptyClass>.size // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)
class SampleClass {
let number: Int64 = 0
let flag = false
}
MemoryLayout<SampleClass>.size // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)
类是引用类型,因此 MemoryLayout 的 size 为 8 个字节。
如果你想更详细地了解内存布局,欢迎查看 Mike Ash 的演讲,探索 Swift 内存布局。
在 Unsafe Swift 中使用指针
指针封装了一个内存地址。涉及直接内存访问的类型会获得一个 unsafe 的前缀,因此指针类型名称是 UnsafePointer。
这可能看起来很烦人,但它提醒开发者正在访问编译器未检查的内存。如果操作不当,可能会导致未定义的行为(Undefined Behavior),而不仅仅是可预测的崩溃。
Swift 的 UnsafePointer 不像 C 那样只提供一种以非结构化方式访问内存的单一类型,像 char *。Swift 包含近十种指针类型,每种都有不同的功能和用途。
开发者需要为其目的使用最合适的指针类型。这可以更好地传达意图、不易出错、避免未定义的行为。
不安全的 Swift 指针使用可预测的命名方案来描述指针的特征:mutable 或 immutable、raw 或类型化、 buffer 样式与否。总共有八种指针组合。
| Pointer Name | Unsafe? | Write Access? | Collection? | Strideable? | Typed? |
|---|---|---|---|---|---|
UnsafeMutablePointer<T> | yep | yep | nope | yep | yep |
UnsafePointer<T> | yep | nope | nope | yep | yep |
UnsafeMutableBufferPointer<T> | yep | yep | yep | nope | yep |
UnsafeBufferPointer<T> | yep | nope | yep | nope | yep |
UnsafeMutableRawPointer | yep | yep | nope | yep | nope |
UnsafeRawPointer | yep | nope | nope | yep | nope |
UnsafeMutableRawBufferPointer | yep | yep | yep | nope | nope |
UnsafeRawBufferPointer | yep | nope | yep | nope | nope |
Unsafe[Mutable][Raw][Buffer]Pointer[<T>]指针是内存地址,直接内存访问是
Unsafe;Mutable意味可以写入;Raw表示它指向一个字节块;Buffer意味着它像一个集合一样工作;<T>是类型化的指针。
使用原始(Raw)指针
我们使用 Unsafe Swift 指针来存储和加载两个整数:
// 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(
byteCount: byteCount,
alignment: alignment)
// 4
defer {
pointer.deallocate()
}
// 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)")
}
}
Raw pointers
byte 0: 42
byte 1: 0
byte 2: 0
byte 3: 0
byte 4: 0
byte 5: 0
byte 6: 0
byte 7: 0
byte 8: 6
byte 9: 0
byte 10: 0
byte 11: 0
byte 12: 0
byte 13: 0
byte 14: 0
byte 15: 0
在上述代码中:
-
定义一些常量,这些常量包含:
count保存要存储的整数个数。stride持有Inttype 的stride。alignment持有Inttype 的alignment。byteCount保存所需的总字节数。
-
添加了
do范围级别,我们可以在后续示例中重用变量名称。 -
UnsafeMutableRawPointer.allocate分配所需的字节。此方法返回一个UnsafeMutableRawPointer. 该类型的名称告诉我们此指针可以 load、store 或 mutate 原始字节。 -
defer块确保可以正确地释放指针。这里需要手动进行内存管理而无法 依赖 ARC。 -
storeBytes并且load,是存储和加载字节。我们可以通过advanced指针stride字节来计算第二个整数的内存地址。由于指针是Strideable的,我们还可以使用指针算术,例如:(pointer + stride).storeBytes(of: 6, as: Int.self)。 -
一个
UnsafeRawBufferPointer让我们像访问字节集合一样访问内存。这意味着我们可以遍历字节并使用下标访问它们。我们还可以使用高阶函数,例如filter、map和reduce等。我们使用原始指针初始化缓冲区指针。
即使 UnsafeRawBufferPointer 不安全,但我们仍然可以通过将其限制为特定类型来使其更安全。
使用类型化的指针
我们可以使用类型化指针来简化前面的示例:
do {
print("Typed pointers")
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count)
defer {
pointer.deinitialize(count: count)
pointer.deallocate()
}
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)")
}
}
Typed pointers
value 0: 42
value 1: 6
请注意该示例的差异:
- 我们使用带泛型参数的
UnsafeMutablePointer.allocate让 Swift 知道正在使用指针来加载和存储Inttype 的值。 - 我们必须在使用前初始化类型化内存并在使用后取消初始化。我们可以分别使用
initialize和deinitialize方法来执行此操作。只有非平凡类型(Non-trivial Type) 才需要取消初始化。但是这些是让我们的代码可以更好的面向未来,以防后期我们改变一些非平凡的类型,这通常不会有额外的成本,编译器会对其进行优化。 - 类型化指针有一个
pointee属性,它提供了一种类型安全的方式来加载和存储值。 - advance 类型化指针时,我们可以简单地说明要 advance 的值的数量。指针可以根据它指向的值的类型来计算正确的步幅。同样,指针算术也有效。我们也可以使用
(pointer + 1).pointee = 6。 - 类型化的缓冲区指针也是如此:它们迭代值而不是字节。
接下来我们将了解如何从不受约束的 UnsafeRawBufferPointer 到更安全的类型约束 UnsafeRawBufferPointer。
将原始指针转换为类型化指针
我们并不总是需要直接初始化类型化指针,也可以从原始指针派生它们:
do {
print("Converting raw pointers to typed pointers")
let rawPointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
defer {
rawPointer.deallocate()
}
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(repeating: 0, count: count)
defer {
typedPointer.deinitialize(count: count)
}
typedPointer.pointee = 42
typedPointer.advanced(by: 1).pointee = 6
typedPointer.pointee
typedPointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value (index): (value)")
}
}
Converting raw pointers to typed pointers
value 0: 42
value 1: 6
此示例与上一个示例类似,不同之处在于它首先创建了一个原始指针。 通过将内存绑定到所需的 Int 类型来创建类型化指针。通过绑定内存(BindMemory) ,我们可以以类型安全的方式访问它。
获取字节实例
如果我们有一个类型的现有实例,并且想要检查它的字节。我们可以使用名为 withUnsafeBytes(of:) 的方法来实现此目的:
do {
print("Getting the bytes of an instance")
var sampleStruct = SampleStruct(number: 25, flag: true)
withUnsafeBytes(of: &sampleStruct) { bytes in
for byte in bytes {
print(byte)
}
}
}
Getting the bytes of an instance
25
0
0
0
1
这将打印出 SampleStruct 实例的原始字节。withUnsafeBytes(of:) 使我们可以在闭包内使用 UnsafeRawBufferPointer。withUnsafeBytes 也可用作 Array 和 Data 的实例方法。
计算校验和
使用 withUnsafeBytes(of:) 可以返回一个结果。 例如我们可以使用它来计算结构中字节的 32 位校验和:
do {
print("Checksum the bytes of a struct")
var sampleStruct = SampleStruct(number: 25, flag: true)
let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in
return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) }
}
print("checksum", checksum) // prints checksum 4294967269
}
reduce 添加字节,然后 ~ 翻转位。 上述代码显示了这个概念。
Unsafe 的注意事项
在编写 Unsafe 代码时要小心避免未定义的行为。以下是一些错误代码的示例:
不要从 withUnsafeBytes 返回指针
// Rule #1
do {
print("1. Don't return the pointer from withUnsafeBytes!")
var sampleStruct = SampleStruct(number: 25, flag: true)
let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
return bytes // strange bugs here we come ☠️☠️☠️
}
print("Horse is out of the barn!", bytes) // undefined!!!
}
你不应该让指针脱离 withUnsafeBytes(of:) 闭包。 即使代码可能可以工作,将来也可能会导致奇怪的错误。
一次只能绑定一种类型
// Rule #2
do {
print("2. Only bind to one type at a time!")
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)
// Breakin' the Law... Breakin' the Law (Undefined behavior)
let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)
// If you must, do it this way:
typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
(boolPointer: UnsafeMutablePointer<Bool>) in
print(boolPointer.pointee) // See Rule #1, don't return the pointer
}
}
永远不要一次将内存绑定到两个不相关的类型。相反,使用 withMemoryRebound(to:capacity:) 之类的方法临时重新绑定内存。此外,将普通类型(如 Int)重新绑定到非普通类型(如类)是非法的,不要这样做。
不要越界
// Rule #3... wait
do {
print("3. Don't walk off the end... whoops!")
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1)
// OMG +1????
for byte in bufferPointer {
print(byte) // pawing through memory like an animal
}
}
始终存在的一个错误的问题,在 Unsafe 代码中会变得更加严重。
Unsafe Swift 示例 1:压缩
我们将使用 Unsafe Swift 来包装 C API。 Cocoa 包含一个 C 模块,它实现了一些常见的数据压缩算法。 这些包括:
- LZ4 适用于速度很重要的情况。
- LZ4A 适用于需要最高压缩比且不关心速度的情况。
- ZLIB,平衡空间和速度。
- 新的开源的 LZFSE ,可以更好地平衡空间和速度。
首先,我们将使用 Data 定义一个纯 Swift API:
import Foundation
import Compression
enum CompressionAlgorithm {
case lz4 // speed is critical
case lz4a // space is critical
case zlib // reasonable speed and space
case lzfse // better speed and space
}
enum CompressionOperation {
case compression, decompression
}
/// return compressed or uncompressed data depending on the operation
func perform(
_ operation: CompressionOperation,
on input: Data,
using algorithm: CompressionAlgorithm,
workingBufferSize: Int = 2000)
-> Data? {
return nil
}
执行压缩和解压缩的函数,当前返回 nil。接下来添加代码:
/// Compressed keeps the compressed data and the algorithm
/// together as one unit, so you never forget how the data was
/// compressed.
struct Compressed {
let data: Data
let algorithm: CompressionAlgorithm
init(data: Data, algorithm: CompressionAlgorithm) {
self.data = data
self.algorithm = algorithm
}
/// Compresses the input with the specified algorithm. Returns nil if it fails.
static func compress(
input: Data,with algorithm: CompressionAlgorithm)
-> Compressed? {
guard let data = perform(.compression, on: input, using: algorithm) else {
return nil
}
return Compressed(data: data, algorithm: algorithm)
}
/// Uncompressed data. Returns nil if the data cannot be decompressed.
func decompressed() -> Data? {
return perform(.decompression, on: data, using: algorithm)
}
}
Compressed 结构体储压缩数据和用于创建算法。这使得在决定使用哪种解压缩算法时不容易出错。接下来添加代码:
/// For discoverability, adds a compressed method to Data
extension Data {
/// Returns compressed data or nil if compression fails.
func compressed(with algorithm: CompressionAlgorithm) -> Compressed? {
return Compressed.compress(input: self, with: algorithm)
}
}
// Example usage:
let input = Data(Array(repeating: UInt8(123), count: 10000))
let compressed = input.compressed(with: .lzfse)
compressed?.data.count // in most cases much less than original input count
let restoredInput = compressed?.decompressed()
input == restoredInput // true
我们添加了一个名为 compressed(with:) 的方法,该方法返回一个可选的 Compressed 结构体。 这个方法简单地调用 Compressed 上的静态方法 compress(input:with:)。
最后有一个例子,但它目前不起作用。向上滚动到我们输入的第一个代码块并开始执行 perform(_:on:using:workingBufferSize:) 在return nil 之前插入以下内容:
// set the algorithm
let streamAlgorithm: compression_algorithm
switch algorithm {
case .lz4: streamAlgorithm = COMPRESSION_LZ4
case .lz4a: streamAlgorithm = COMPRESSION_LZMA
case .zlib: streamAlgorithm = COMPRESSION_ZLIB
case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
}
// set the stream operation and flags
let streamOperation: compression_stream_operation
let flags: Int32
switch operation {
case .compression:
streamOperation = COMPRESSION_STREAM_ENCODE
flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
case .decompression:
streamOperation = COMPRESSION_STREAM_DECODE
flags = 0
}
这会将我们的 Swift 类型转换为压缩算法所需的 C 类型。接下来,将 return nil 替换为:
// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
streamPointer.deallocate()
}
// 2: initialize the stream
var stream = streamPointer.pointee
var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
guard status != COMPRESSION_STATUS_ERROR else {
return nil
}
defer {
compression_stream_destroy(&stream)
}
// 3: set up a destination buffer
let dstSize = workingBufferSize
let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
defer {
dstPointer.deallocate()
}
return nil // To be continued
在上述代码中:
- 分配一个
compression_stream并用 defer 处理释放。 - 使用
pointee属性,获取stream并将其传递给compression_stream_init函数。 编译器在这里做了一些特别的事情:它使用in-out &标记来获取您的compression_stream并将其转换为UnsafeMutablePointer<compression_stream>。 或者我们可以通过使用streamPointer。 那么就不需要这种特殊的转换了。 - 最后,您创建一个目标缓冲区作为工作缓冲区。
接下来,通过将最终的 return nil 替换为:
// process the input
return input.withUnsafeBytes { srcRawBufferPointer in
// 1
var output = Data()
// 2
let srcBufferPointer = srcRawBufferPointer.bindMemory(to: UInt8.self)
guard let srcPointer = srcBufferPointer.baseAddress else {
return nil
}
stream.src_ptr = srcPointer
stream.src_size = input.count
stream.dst_ptr = dstPointer
stream.dst_size = dstSize
// 3
while status == COMPRESSION_STATUS_OK {
// process the stream
status = compression_stream_process(&stream, flags)
// collect bytes from the stream and reset
switch status {
case COMPRESSION_STATUS_OK:
// 4
output.append(dstPointer, count: dstSize)
stream.dst_ptr = dstPointer
stream.dst_size = dstSize
case COMPRESSION_STATUS_ERROR:
return nil
case COMPRESSION_STATUS_END:
// 5
output.append(dstPointer, count: stream.dst_ptr - dstPointer)
default:
fatalError()
}
}
return output
}
上述代码中:
- 创建一个包含输出的 Data 对象——压缩或解压缩的数据,具体取决于这是什么操作。
- 使用我们分配的指针及其大小设置源缓冲区和目标缓冲区。
- 在这里,除非它返回
COMPRESSION_STATUS_OK,否则就一直调用compression_stream_process。 - 我们将目标缓冲区复制到最终从此函数返回的输出中。
- 当最后一个数据包进来时,用
COMPRESSION_STATUS_END标记,我们可能只需要复制目标缓冲区的一部分。
在此示例中,我们可以看到 10,000 个元素的数组被压缩到 153 个字节。
Unsafe Swift 示例 2:随机数
随机数对于许多应用都很重要。macOS 提供 arc4random,它可以产生加密的随机数。但是这个调用在 Linux 上不可用。 此外,arc4random 仅提供 UInt32 形式的随机数。 /dev/urandom 提供了无限的良好随机数来源。我们将读此文件并创建类型安全的随机数。
import Foundation
enum RandomSource {
static let file = fopen("/dev/urandom", "r")!
static let queue = DispatchQueue(label: "random")
static func get(count: Int) -> [Int8] {
let capacity = count + 1 // fgets adds null termination
var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
defer {
data.deallocate()
}
queue.sync {
fgets(data, Int32(capacity), file)
}
return Array(UnsafeMutableBufferPointer(start: data, count: count))
}
}
系统中仅存在一个该文件,我们将文件变量声明为静态。当进程退出时,我们将依赖系统将其关闭。由于多个线程可能需要随机数,因此我们需要使用串行 GCD 队列来保护对它的访问。get 函数是实际工作发生的地方。首先,创建一个超出我们需要的未分配存储,因为 fgets 始终以 0 终止。接下来,从文件中获取数据,确保在对 GCD 队列进行操作时这样做。最后,将数据复制到标准数组,首先将其包装在可以充当序列的 UnsafeMutableBufferPointer 中。
到目前为止,这只会给你一个 Int8 值的数组。 现在我们要扩展它:
extension BinaryInteger {
static var randomized: Self {
let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
return numbers.withUnsafeBufferPointer { bufferPointer in
return bufferPointer.baseAddress!.withMemoryRebound(
to: Self.self,
capacity: 1) {
return $0.pointee
}
}
}
}
Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized
这为 BinaryInteger 协议的所有子类型添加了一个静态随机属性。首先,我们得到随机数,使用返回的数组字节,然后将 Int8 值重新绑定为请求的类型并返回一个副本。我们现在正在以安全的方式生成随机数。