前言
此篇文章背景源自一次偶现高频次崩溃问题排查。底层长连接通信采用 Rust 编写,涉及与业务层的桥接:Rust <-> C <-> Swift
,虽说 Rust 与 Swift 都以安全著称,但不管是 Rust FFI 到 C 还是 Swift 与 C 的交互,代码中都不得不触及unsafe
关键字,也是这次崩溃问题的原因所在,这就不难理解为什么 Swift 和 Rust 的设计者毫不保留地采用Unsafe-
命名与unsafe
关键字了。
![](https://user-gold-cdn.xitu.io/2019/9/14/16d2cef2e1ee2b06?w=732&h=164&f=jpeg&s=21782)
// typedef struct {
// const uint8_t *array;
// size_t len;
// } ByteArray;
// ......
// 问题代码
let bodyToSend = bodyData.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) in
bytes
}
// 修复代码
let bodyToSend = [UInt8](bodyData)
// 调用 C 方法与 Rust 交互
let len = bodyData.count
let bodyByteArray = ByteArray(array: bodyToSend, len: len)
halo_send_message(1, namespace, path, metadataToSend, bodyByteArray)
// ......
TL;DR - 结论
withUnsafe-
方法中获取的指针一定不要让其"逃出"「不安全区」,仅在所属不安全闭包中使用,否则该指针将不再受控制,导致不可预测的问题,如崩溃。- 一定不要隐式获取变量的不安全指针,这会隐藏上述结论 1. 中的问题,更难以察觉。
let bodyToSend = UnsafePointer(&bodyData) // 一定不要这么获取变量指针
Swift 的安全性
想必稍微对 Swift 语言有所了解都会知道这是一门安全的编程语言,因此,在谈及其不安全的部分之前,先说说它的安全性:
- 内存安全 ❤️
- 不可访问未初始化的内存 💘
- 防止野指针访问,数组无法越界 💖
- 避免未定义行为 💕
关于安全性,Swift 语言的设计者们对此的定义不是不崩溃,而是:
永远不会在无意中访问错误的内存
为此,Swift 做两件事,一是让编译器时刻提醒开发者注意安全;二则是开发者强行开车导致产生未定义的行为的话,立即原地爆炸💥,避免更严重的问题发生。
不安全的 Swift - UnsafePointer
既然 Swift 追求安全,为什么要设计不安全的部分呢?通俗地讲,就是允许有经验的老司机开黑车:防抱死功能一关,请开始你的表演。
- 为超高性能实现提供方案,安全与高性能常常需要权衡与妥协
- 与其它非安全的语言,如 C,进行交互,包括直接访问硬件
Swift 内存分配与布局
认识UnsafePoint
前,我们先来了解下 Swift 如何对内存进行分配与布局的。
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<String>.size // returns 16
MemoryLayout<String>.alignment // returns 8
MemoryLayout<String>.stride // returns 16
let zero = 0.0
MemoryLayout.size(ofValue: zero) // return 8, zero as Double implictly
struct EmptyStruct {}
MemoryLayout<EmptyStruct>.size // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride // returns 1
struct SampleStruct {
var number: UInt32
var flag: Bool // {
// didSet {
// print("wow")
// }
// }
}
MemoryLayout<SampleStruct>.size // returns 5
MemoryLayout<SampleStruct>.alignment // returns 4
MemoryLayout<SampleStruct>.stride // returns 8
MemoryLayout.offset(of: \SampleStruct.flag) // return 4 without didSet; return nil with didSet
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: Bool = 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.offset(of: \SampleClass.flag) // return nil
为了解 Swift 中的布局情况,我们用到了自带的MemoryLayout
工具类。
MemoryLayout<T>.size
:一个 T 类型数据实例所占的连续内存大小,单位:bytesMemoryLayout<T>.alignment
:数据类型 T 数据类型的对齐原则大小,单位:bytesMemoryLayout<T>.stride
:一个 T 类型数组中,任意一个元素从开始地址到下一个元素的开始地址所占用的连续内存大小,单位:bytes
UnsafePointer 类型
在 Swift 中指针是几种以Unsafe-
前缀与-Pinter
后缀命名的结构体,看得出来,尽管是非安全的指针操作API,Swift 也希望能尽可能地做到安全。
UnsafePointer<T>
: 对应于const T *
UnsafeMutablePointer<T>
:对应于T *
UnsafeRawPointer
: 对应于const void *
UnsafeMutableRawPointer
:对应于void *
泛型指针与原始指针
原始指针
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let count = 2
let byteCount = stride * count
let pointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment) // 原始指针对类型无感知,故指定 byteCount 与 alignment,通过 MemoryLayout 获得
defer {
pointer.deallocate()
}
// 原始指针操作均须额外指定类型
pointer.storeBytes(of: 0b111111111111, as: Int.self)
pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
(pointer+stride).storeBytes(of: 6, as: Int.self)
pointer.load(as: Int.self)
pointer.advanced(by: stride).load(as: Int.self)
(pointer+stride).load(as: Int.self)
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
for (offset, byte) in bufferPointer.enumerated() {
print("byte \(offset): \(byte)")
}
泛型指针
let count = 2
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count) // 只需初始值与指定类型实例数量,类似泛型数组的初始化
defer {
pointer.deinitialize(count: count)
pointer.deallocate()
}
// 操作无须额外指定类型,通过泛型推断
pointer.pointee = 0b111111111111
pointer.advanced(by: 1).pointee = 6
(pointer+1).pointee = 6
pointer.pointee
pointer.advanced(by: 1).pointee
(pointer+1).pointee
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for (offset, value) in bufferPointer.enumerated() {
print("value \(offset): \(value)")
}
原始指针与泛型指针的转换
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count
// Converting raw pointers to typed pointers
let rawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
defer {
rawPointer.deallocate()
}
// 将原始指针转换为泛型指针,同一地址空间仅可 bindMemory 一次
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(repeating: 0, count: count)
defer {
typedPointer.deinitialize(count: count)
}
typedPointer.pointee = 0b111111111111
typedPointer.advanced(by: 1).pointee = 6
typedPointer.pointee
typedPointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
for (offset, value) in bufferPointer.enumerated() {
print("value \(offset): \(value)")
}
可变性与不可变性
Swift 中通过 let
和 var
关键字区分变量的可变性,指针中也采用了类似方案,让开发者针对性地控制指针的可变性,即其指向的内存块的可写性。
Buffer 指针
Buffer 指针,其本质就是一个指针+一个大小count
,即一串连续的内存块。
指针使用过程中的一些重要原则 ⚠️
Swift 是一门类型安全的语言,但当代码中出现Unsafe
字样时,务必遵循以下一些指针操作原则,以避免未定义行为发生,否则遇到问题时将非常难以定位。
- 指针使用前,务必分配内存并初始化
- 务必释放已分配的内存
- 务必恢复已初始化的内存
- 千万别在
withUnsafe-
方法中返回获取的指针 bindMemory
一次仅可绑定一种类型- 指针操作不要「越界 」
- 不要多次释放或逆初始化同一块内存