默认情况下,Swift是内存安全的,这就意味着 Swift 会避免内存的直接访问,并确定所有变量会在初始化后才进行使用。这里的关键词语是“默认的”。也就是说,当我们需要时,不安全的 Swift 还是可以通过指针来直接访问到内存地址的
这篇教程会告诉你为啥 Swift 又是“不安全的”。“不安全”这个词可能会使人误解,它不是说我们写的代码可能会发生出乎意料的问题,而是说我们需要额外注意这些代码,因为编译器在编译过程中并不会给予我们帮助,我们无法直观地捕获到错误。
在开发工作中,当你需要与C这些不安全的语言进行交互时,就会用得上这篇文章所介绍的特性了。
开始
这篇教程由3个 playgrounds 组成。在第一个 playground 中,将会创建几个简短的片段来了解内存布局并且使用不安全的指针。在第二个 playground 中,会封装C语言的API来执行流数据的压缩。最后的 playground ,创建arc4random
的替代函数
首先创建一个新的 playground,名称为UnsafeSwift
内存布局
不安全的 Swift 会直接在内存系统中进行交互。内存可以通过一系列格子来进行可视化,每个格子里是与内存相关的唯一性的内存地址。存储的最小单位是 byte,由8个 bits 组成。8 bit 的 byte 可以保存0-255大小的数字。
Swift 有一个MemoryLayout
的类可以告诉我们每个类型对象的大小和分布。添加如下代码:
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>
会在编译时确定指定类型的size
,alignment
和stride
。举个例子来说, Int16
的size
是2个byte,内存对齐也是2.这就意味着其内存地址必须是偶数地址
因此,假设地址100和101给Int16
分配地址的话,肯定就是选择100了, 因为101违背了对齐原则。当我们将一堆Int16
打包在一起的话,stride
表示的是,当前类型的内存地址开头与下一个内存地址开头之间的距离
接下来,看看用户自定义的structs
的内存布局:
MemoryLayout<EmptyStruct>.size // 0
MemoryLayout<EmptyStruct>.alignment // 1
MemoryLayout<EmptyStruct>.stride // 1
struct SampleStruct {
let number: UInt32
let flag: Bool
}
MemoryLayout<SampleStruct>.size // 5
MemoryLayout<SampleStruct>.alignment // 4
MemoryLayout<SampleStruct>.stride // 8
复制代码
空的结构体的size为0.因为空结构体的stride
为1,所以它可以分配在任意的地址上。
对于SampleStruct来说, 其size
为5,stride
为8.这是因为内存对齐的位数是4个字节。
然后看下类对象的:
class EmptyClass {}
MemoryLayout<EmptyClass>.size // 8
MemoryLayout<EmptyClass>.alignment // 8
MemoryLayout<EmptyClass>.stride // 8
class SampleClass {
let number: Int64 = 0
let flag: Bool = false
}
MemoryLayout<SampleClass>.size // 8
MemoryLayout<SampleClass>.alignment // 8
MemoryLayout<SampleClass>.stride // 8
复制代码
可以看到,类对象的size
,alignment
和stride
都是8,且不管是否空的对象。
指针
指针对象包含了一个内存地址。直接涉及内存访问的类型会有一个unsafe
的前缀,所以其指针称为UnsafePointer
. 虽然长长的类型看起来会比较烦,但是可以使我们清楚地知道相关的代码是没有经过相关的编译器检查,可能会导致未定义的行为(而不仅仅是一个可预见的崩溃)。
Swift 的设计者其实可以创建了一个UnsafePointer
类型,并且该类型与C语言中的char *
相等,可以用来以非结构化方式来访问内存。但是他们并没有。Swift 涵盖了大部分的指针类型,每种类型都有不同的用处和目的。使用合适的指针类型可以更好地达到我们的需求,更少地引起错误。
不安全的 Swift 指针的命名可以让我们知道该指针的特征。可变( Mutable )或者不可变( Immutable ), 原始的( raw ) 或者其他类型的( typed ),是否是缓存风格( buffer style ). 在 Swift 中,一共有8种类型组合:
Unsafe[Mutable][Raw][Buffer]Pointer[]
指针就是内存地址,直接访问内存就是 Unsafe 的
Mutable 表示可写
Raw 表示它是否指向了二进制数据类型的字节(blob of bytes)
Buffer 表示其是否是一个结合
原始指针的使用
// 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)")
}
}
// Output
// 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
复制代码
上面的例子中,使用不安全的 Swift 指针来保存并加载两个整数:
-
常量:
count
: 要保存的整数的个数stride
:Int
的步长alignment
:Int
的内存对齐位数byteCount
: 总字节数
-
do
添加了一个块级作用域,方便重新使用变量名 -
UnsafeMutableRawPointer.allocate
用于分配所需要的字节数。该方法返回一个UnsafeMutableRawPointer
指针。从名称我们可以得知该指针可以用来加载和保存原始类型的字节 -
defer
用来保证指针能够得到释放。 -
storeBytes
和load
用于存储和加载字节。第二个整数的内存地址根据指针的步长进行计算得出。 -
UnsafeRawBufferPointer
让我们可以像访问字节集合一样来对内存地址进行访问。就是我们可以遍历字节,通过下标访问,或者是调用map
,filter
等方法。缓存区的指针需要使用原始指针来进行初始化
类型指针的使用
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)")
}
}
// Output
// Typed pointers
// value 0: 42
// value 1: 6
复制代码
与原始指针的不同点在于:
UnsafeMutablePointer.allocate
方法用于分配内存。- 类型对象的内存必须在使用前进行初始化,不再使用以后需要进行析构处理。
- 类型指针有一个属性
pointee
用于加载和保存值 - 当向前移动类型指针时,可以很方便标志指针所指向的位置。指针会根据其指向的类型,计算出正确地步长。
- 类型的 buffer 指针也可以遍历指针对象
原始指针转换为类型指针
类型指针并不一定需要直接进行初始化,也可以通过原始指针来进行转换:
do {
print("Converting raw pointers to typed pointers")
let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
defer {
rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
}
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(to: 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)")
}
}
// Output
// Converting raw pointers to typed pointers
// value 0: 42
// value 1: 6
复制代码
这个例子除了首先创建了原始指针然后将内存绑定到类型指针上以外,与上一个相似。绑定内存后,我们就能通过一种类型安全的方式对内存进行访问。内存绑定在我们创建类型指针时会隐式进行
获取实例的字节数
一般情况下,我们可以通过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)
}
}
}
// Output
// 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)
}
复制代码
Unsafe 的使用规则
- 不要在
withUnsafeBytes
中返回指针 - 一次只绑定一种类型
- 避免指针指向最后的位置