前言
Objective-C和C語言經常需要使用到指針。Swift中的數據類型由于良好的設計,使其可以和基于指針的C語言API無縫混用。但是語法上有很大的差別。
默認情況下,Swift 是內存安全的,這意味著它禁止我們直接操作內存,并且確保所有的變量在使用前都已經被正確地初始化了。但是,Swift 也提供了我們使用指針直接操作內存的方法,直接操作內存是很危險的行為,很容易就出現錯誤,因此官方將直接操作內存稱為 “unsafe 特性”。
一旦我們開始直接操作內存,一切就得靠我們自己了,因為在這種情況下編譯能給我們提供的幫助實在不多。正常情況下,我們在與 C 進行交互,或者我們需要挖掘 Swift 內部實現原理的時候會需要使用到這個特性。
Memory Layout
Swift 提供了 MemoryLayout
來檢測特定類型的大小以及內存對齊大小:
MemoryLayout<Int>.size // return 8 (on 64-bit)MemoryLayout<Int>.alignment // return 8 (on 64-bit)MemoryLayout<Int>.stride // return 8 (on 64-bit)MemoryLayout<Int16>.size // return 2MemoryLayout<Int16>.alignment // return 2MemoryLayout<Int16>.stride // return 2MemoryLayout<Bool>.size // return 2MemoryLayout<Bool>.alignment // return 2MemoryLayout<Bool>.stride // return 2MemoryLayout<Float>.size // return 4MemoryLayout<Float>.size // return 4MemoryLayout<Float>.alignment // return 4MemoryLayout<Double>.stride // return 8MemoryLayout<Double>.alignment // return 8MemoryLayout<Double>.stride // return 8
MemoryLayout<Type>
是一個用于在編譯時計算出特定類型(Type)的 size
, alignment
以及 stride
的泛型類型。返回的數值以字節為單位。例如 Int16
類型的大小為 2 個字節,內存對齊為 2 個字節以及當我們需要連續排列多個 Int16
類型時,每一個 Int16
所需要占用的大小(stride)為 2 個字節。所有基本類型的 stride
都與 size
是一致的。
接下來,看看結構體類型的 MemoryLayout:
struct EmptyStruct {}MemoryLayout<EmptyStruct>.size // returns 0MemoryLayout<EmptyStruct>.alignment // returns 1MemoryLayout<EmptyStruct>.stride // returns 1struct SampleStruct { let number: UInt32 let flag: Bool}MemoryLayout<SampleStruct>.size // returns 5MemoryLayout<SampleStruct>.alignment // returns 4MemoryLayout<SampleStruct>.stride // returns 8
空結構體的大小為 0,內存對齊為 1, 表明它可以存在于任何一個內存地址上。有趣的是 stride
為 1,這是因為盡管結構為空,但是當我們使用它創建一個實例的時候,它也必須要有一個唯一的地址。
對于 SampleStruct
,它所占的大小為 5,但是 stride 為 8。這是因為編譯需要為其填充空白的邊界,使其符合它的 4 字節內存邊界對齊。
再來看看類:
class EmptyClass {}MemoryLayout<EmptyClass>.size // returns 8 (on 64-bit)MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)MemoryLayout<EmptyClass>.stride // 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>.aligment // returns 8 (on 64-bit)MemoryLayout<SampleClass>.stride // returns 8 (on 64-bit)
由于類都是引用類型,所以它所有的大小都是 8 字節。
關于 MemoryLayout 的更多詳細信息可以參考 Mike Ash 的演講。
指針
一個指針就是對一個內存地址的封裝。在 Swift 當中直接操作指針的類型都有一個 “unsafe” 前綴,所以它的指針類型稱為 UnsafePointer
。這個前綴似乎看起來很令人惱火,不過這是 Swift 在提醒你,你現在正在跨越雷池,編譯器不會對這種操作進行檢查,你需要對自己的代碼承擔全部的責任。
Swift 中包含了一打類型的指針類型,每個類型都有它們的作用和目的,使用適當的指針類型可以防止錯誤的發生并且更清晰地表達開發者的意圖,防止未定義行為的產生。
Swift 的指針類型使用了很清晰的命名,我們可以通過名字知道這是一個什么類型的指針。可變或者不可變,原生(raw)或者有類型的,是否是緩沖(buffer)類型,這三種特性總共組合出了 8 種指針類型。
接下來的幾個小節會詳細介紹這幾種指針類型。
使用原生(Raw)指針
在 Playground 中添加如下代碼:
// 1let count = 2let stride = MemoryLayout<Int>.stridelet alignment = MemoryLayout<Int>.alignmentlet byteCount = stride * count // 2do { 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)") }}
在這個代碼段中,我們使用了 Unsafe Swift 指針去存儲和讀取兩個整型數值。
接下來是對這段代碼的解釋:
1、聲明了接下來都會用到的幾個常量:
count
表示了我們要存儲的整數的個數stride
表示了 Int 類型的 stridealignment
表示了 Int 類型的內存對齊byteCount
表示占用的全部字節數 2、使用 do
來增加一個作用域,讓我們可以在接下的示例中復用作用域中的變量名
3、使用 UnsafeMutableRawPointer.allocate
方法來分配所需的字節數。我們使用了 UnsafeMutableRawPointer
,它的名字表明這個指針可以用來讀取和存儲(改變)原生的字節。
4、使用 defer
來保證內存得到正確地釋放。操作指針的時候,所有內存都需要我們手動進行管理。
5、storeBytes
和 load
方法用于存儲和讀取字節。第二個整型數值的地址通過對 pointer
的地址前進 stride
來得到。因為指針類型是 Strideable
的,我們也可以直接使用指針算術運算 (pointer+stride).storeBytes(of: 6, as: Int.self
)。
6、UnsafeRawBufferPointer
類型以一系列字節的形式來讀取內存。這意味著我們可以這些字節進行迭代,對其使用下標,或者使用 filter
,map
以及 reduce
這些很酷的方法。緩沖類型指針使用了原生指針進行初始化。
使用類型指針
我們可以使用類型指針實現跟上面代碼一樣的功能,并且更簡單:
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)") }}
注意到以下幾點不同:
UnsafeMutablePointer.allocate
進行內存的分配。指定的泛型參數讓 Swift 知道我們將會使用這個指針來存儲和讀取 Int 類型的值。initialize
和 deinitialize
方法。pointee
屬性,它可以以類型安全的方式讀取和存儲值。stride
值。同樣的,我們可以直接對指針進行算術運算 (pointer + 1).pointee = 6
。將原生指針轉換為類型指針
類型指針并不總是使用初始化得到的,它們可以從原生指針中轉化而來。
在 Playground 中添加如下代碼:
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)") }}
這段代碼與上一段類似,除了它先創建了原生指針。我們通過將內存綁定(binding)到指定的類型上來創建類型指針。通過對內存的綁定,我們可以通過類型安全的方法來訪問它。將我們手動創建類型指針的時候,系統其實自動幫我們進行了內存綁定。
獲取一個實例的字節
很多時候我們需要從一個現存的實例里獲取它的字節。這時可以使用 withUnsafeBytes(of:)
方法。
在 Playground 中添加如下代碼:
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) } }}
這段代碼會打印出 SampleStruct
實例的原生字節。withUnsafeBytes(of:)
方法可以獲取到 UnsafeRawBufferPointer
并傳入閉包中供我們使用。
withUnsafeBytes
同樣適合用 Array
和 Data
的實例。
使用 Swift 操作指針的三大原則
當我們使用 Swift 操作指針的時候必須加倍小心,防止寫出未定義行為的代碼。下面是幾個壞代碼的示例。
不要從 withUnsafeBytes 中返回指針
// Rule #1do { 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 #2do { 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(bytes: byteCount, alignedTo: 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)重新綁定到一個自定義類型(如 class)上。不要做這種傻事。### 不要操作超出范圍的內存```swift/137587.html">swift// Rule #3... waitdo { 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(bytes: byteCount, alignedTo: alignment) let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1???? for byte in bufferPointer { print(byte) // pawing through memory like an animal }}
這是最糟糕的一種錯誤了,請再三檢查你的代碼,保證不要有這種情況出現。切記。
示例:隨機數生成
隨機數在很多地方都有重要的作用,從游戲到機器學習。macOS 提供了 arc4random
方法用于隨機數生成。不幸的是,這個方法無法在 Linux 上使用。并且,arc4random
方法只提供了 UInt32 類型的隨機數。事實上,/dev/urandom
這個設備文件中就提供了無限的隨機數。
這一小節中,我們將使用指針讀取這個文件,并產生完全類型安全的隨機數。
創建一個新 Playground
,命名為 RandomNumbers
,并確保選擇了 macOS 平臺。
創建完成后,添加如下代碼:
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(capacity: capacity) } queue.sync { fgets(data, Int32(capacity), file) } return Array(UnsafeMutableBufferPointer(start: data, count: count)) }}
為了確保整個系統中只存在一個 file 變量,我們對其使用了 static
修飾符。系統會在我們的進程結束時關閉文件。因為我們有可能在多個線程中同時獲取隨機數,所以需要使用一個串行的 GCD 隊列來進行保護。
get
函數是所有功能完成的地方。首先,我們根據傳入的大小分配了必要的內存,注意這里需要 +1 是因為 fets
函數總是以 /0 結束。接下來,我們就使用 fgets
函數從文件中讀取數據,確保我們在串行隊列中進行讀取操作。最后,我們先將數據封裝為一個 UnsafeMutableBufferPointer
,并將其轉化為一個數組。
在 playground 的最后添加如下代碼:
extension Integer { 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.randomizedUInt8.randomizedInt16.randomizedUInt16.randomizedInt16.randomizedUInt32.randomizedInt64.randomizedUInt64.randomized
這里我們為 Integer 協議添加了一個靜態屬性,并為其提供了默認實現。我們首先獲取了隨機數,隨后我們將獲得字節數組重新綁定為所需要的類型,然后返回它的值。簡單!
就這樣,我們使用 unsafe Swift 實現了一個類型安全的隨機器生成方法。
在日常開發中,我們并不會接觸到很多直接操作內存的情境。但是掌握它的操作,能讓我們在碰到類似代碼里更加從容。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。
新聞熱點
疑難解答