此篇文章背景源自一次偶現高頻次崩潰問題排查。底層長鏈接通訊採用 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)
// ......
複製代碼
withUnsafe-
方法中獲取的指針必定不要讓其"逃出"「不安全區」,僅在所屬不安全閉包中使用,不然該指針將再也不受控制,致使不可預測的問題,如崩潰。let bodyToSend = UnsafePointer(&bodyData) // 必定不要這麼獲取變量指針
複製代碼
想必稍微對 Swift 語言有所瞭解都會知道這是一門安全的編程語言,所以,在談及其不安全的部分以前,先說說它的安全性:swift
關於安全性,Swift 語言的設計者們對此的定義不是不崩潰,而是:數組
永遠不會在無心中訪問錯誤的內存安全
爲此,Swift 作兩件事,一是讓編譯器時刻提醒開發者注意安全;二則是開發者強行開車致使產生未定義的行爲的話,當即原地爆炸💥,避免更嚴重的問題發生。閉包
既然 Swift 追求安全,爲何要設計不安全的部分呢?通俗地講,就是容許有經驗的老司機開黑車:防抱死功能一關,請開始你的表演。編程語言
認識UnsafePoint
前,咱們先來了解下 Swift 如何對內存進行分配與佈局的。ide
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在 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 指針,其本質就是一個指針+一個大小count
,即一串連續的內存塊。
Swift 是一門類型安全的語言,但當代碼中出現Unsafe
字樣時,務必遵循如下一些指針操做原則,以免未定義行爲發生,不然遇到問題時將很是難以定位。
withUnsafe-
方法中返回獲取的指針bindMemory
一次僅可綁定一種類型