Swift中的指針操作詳解
前言
Objective-C和C語言經(jīng)常需要使用到指針。Swift中的數(shù)據(jù)類型由于良好的設(shè)計,使其可以和基于指針的C語言API無縫混用。但是語法上有很大的差別。
默認情況下,Swift 是內(nèi)存安全的,這意味著它禁止我們直接操作內(nèi)存,并且確保所有的變量在使用前都已經(jīng)被正確地初始化了。但是,Swift 也提供了我們使用指針直接操作內(nèi)存的方法,直接操作內(nèi)存是很危險的行為,很容易就出現(xiàn)錯誤,因此官方將直接操作內(nèi)存稱為 “unsafe 特性”。
一旦我們開始直接操作內(nèi)存,一切就得靠我們自己了,因為在這種情況下編譯能給我們提供的幫助實在不多。正常情況下,我們在與 C 進行交互,或者我們需要挖掘 Swift 內(nèi)部實現(xiàn)原理的時候會需要使用到這個特性。
Memory Layout
Swift 提供了 MemoryLayout
來檢測特定類型的大小以及內(nèi)存對齊大?。?/p>
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 2 MemoryLayout<Int16>.alignment // return 2 MemoryLayout<Int16>.stride // return 2 MemoryLayout<Bool>.size // return 2 MemoryLayout<Bool>.alignment // return 2 MemoryLayout<Bool>.stride // return 2 MemoryLayout<Float>.size // return 4 MemoryLayout<Float>.size // return 4 MemoryLayout<Float>.alignment // return 4 MemoryLayout<Double>.stride // return 8 MemoryLayout<Double>.alignment // return 8 MemoryLayout<Double>.stride // return 8
MemoryLayout<Type>
是一個用于在編譯時計算出特定類型(Type)的 size
, alignment
以及 stride
的泛型類型。返回的數(shù)值以字節(jié)為單位。例如 Int16
類型的大小為 2 個字節(jié),內(nèi)存對齊為 2 個字節(jié)以及當我們需要連續(xù)排列多個 Int16
類型時,每一個 Int16
所需要占用的大小(stride)為 2 個字節(jié)。所有基本類型的 stride
都與 size
是一致的。
接下來,看看結(jié)構(gòu)體類型的 MemoryLayout:
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
空結(jié)構(gòu)體的大小為 0,內(nèi)存對齊為 1, 表明它可以存在于任何一個內(nèi)存地址上。有趣的是 stride
為 1,這是因為盡管結(jié)構(gòu)為空,但是當我們使用它創(chuàng)建一個實例的時候,它也必須要有一個唯一的地址。
對于 SampleStruct
,它所占的大小為 5,但是 stride 為 8。這是因為編譯需要為其填充空白的邊界,使其符合它的 4 字節(jié)內(nèi)存邊界對齊。
再來看看類:
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 字節(jié)。
關(guān)于 MemoryLayout 的更多詳細信息可以參考 Mike Ash 的演講。
指針
一個指針就是對一個內(nèi)存地址的封裝。在 Swift 當中直接操作指針的類型都有一個 “unsafe” 前綴,所以它的指針類型稱為 UnsafePointer
。這個前綴似乎看起來很令人惱火,不過這是 Swift 在提醒你,你現(xiàn)在正在跨越雷池,編譯器不會對這種操作進行檢查,你需要對自己的代碼承擔全部的責任。
Swift 中包含了一打類型的指針類型,每個類型都有它們的作用和目的,使用適當?shù)闹羔橆愋涂梢苑乐瑰e誤的發(fā)生并且更清晰地表達開發(fā)者的意圖,防止未定義行為的產(chǎn)生。
Swift 的指針類型使用了很清晰的命名,我們可以通過名字知道這是一個什么類型的指針??勺兓蛘卟豢勺?,原生(raw)或者有類型的,是否是緩沖(buffer)類型,這三種特性總共組合出了 8 種指針類型。
接下來的幾個小節(jié)會詳細介紹這幾種指針類型。
使用原生(Raw)指針
在 Playground 中添加如下代碼:
// 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)") } }
在這個代碼段中,我們使用了 Unsafe Swift 指針去存儲和讀取兩個整型數(shù)值。
接下來是對這段代碼的解釋:
1、聲明了接下來都會用到的幾個常量:
count
表示了我們要存儲的整數(shù)的個數(shù)stride
表示了 Int 類型的 stridealignment
表示了 Int 類型的內(nèi)存對齊byteCount
表示占用的全部字節(jié)數(shù)
2、使用 do
來增加一個作用域,讓我們可以在接下的示例中復用作用域中的變量名
3、使用 UnsafeMutableRawPointer.allocate
方法來分配所需的字節(jié)數(shù)。我們使用了 UnsafeMutableRawPointer
,它的名字表明這個指針可以用來讀取和存儲(改變)原生的字節(jié)。
4、使用 defer
來保證內(nèi)存得到正確地釋放。操作指針的時候,所有內(nèi)存都需要我們手動進行管理。
5、storeBytes
和 load
方法用于存儲和讀取字節(jié)。第二個整型數(shù)值的地址通過對 pointer
的地址前進 stride
來得到。因為指針類型是 Strideable
的,我們也可以直接使用指針算術(shù)運算 (pointer+stride).storeBytes(of: 6, as: Int.self
)。
6、UnsafeRawBufferPointer
類型以一系列字節(jié)的形式來讀取內(nèi)存。這意味著我們可以這些字節(jié)進行迭代,對其使用下標,或者使用 filter
,map
以及 reduce
這些很酷的方法。緩沖類型指針使用了原生指針進行初始化。
使用類型指針
我們可以使用類型指針實現(xiàn)跟上面代碼一樣的功能,并且更簡單:
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
進行內(nèi)存的分配。指定的泛型參數(shù)讓 Swift 知道我們將會使用這個指針來存儲和讀取 Int 類型的值。 - 在使用類型指針前需要對其進行初始化,并在使用后銷毀。這兩個功能分別是使用
initialize
和deinitialize
方法。 - 類型指針提供了
pointee
屬性,它可以以類型安全的方式讀取和存儲值。 - 當需要指針前進的時候,我們只需要指定想要前進的個數(shù)。類型指針會自動根據(jù)它所指向的數(shù)值類型來計算
stride
值。同樣的,我們可以直接對指針進行算術(shù)運算(pointer + 1).pointee = 6
。 - 有類型的緩沖型指針也會直接操作數(shù)值,而非字節(jié)
將原生指針轉(zhuǎn)換為類型指針
類型指針并不總是使用初始化得到的,它們可以從原生指針中轉(zhuǎn)化而來。
在 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)") } }
這段代碼與上一段類似,除了它先創(chuàng)建了原生指針。我們通過將內(nèi)存綁定(binding)到指定的類型上來創(chuàng)建類型指針。通過對內(nèi)存的綁定,我們可以通過類型安全的方法來訪問它。將我們手動創(chuàng)建類型指針的時候,系統(tǒng)其實自動幫我們進行了內(nèi)存綁定。
獲取一個實例的字節(jié)
很多時候我們需要從一個現(xiàn)存的實例里獲取它的字節(jié)。這時可以使用 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
實例的原生字節(jié)。withUnsafeBytes(of:)
方法可以獲取到 UnsafeRawBufferPointer
并傳入閉包中供我們使用。
withUnsafeBytes
同樣適合用 Array
和 Data
的實例。
使用 Swift 操作指針的三大原則
當我們使用 Swift 操作指針的時候必須加倍小心,防止寫出未定義行為的代碼。下面是幾個壞代碼的示例。
不要從 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(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 } } **絕對不要**讓一個內(nèi)存同時綁定兩個不同的類型。如果你需要臨時這么做,可以使用 `withMemoryRebound(to:capacity:)` 來對內(nèi)存進行重新綁定。并且,這條規(guī)則也表明了不要將一個基本類型(如 Int)重新綁定到一個自定義類型(如 class)上。不要做這種傻事。 ### 不要操作超出范圍的內(nèi)存 ```swift // 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(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 } }
這是最糟糕的一種錯誤了,請再三檢查你的代碼,保證不要有這種情況出現(xiàn)。切記。
示例:隨機數(shù)生成
隨機數(shù)在很多地方都有重要的作用,從游戲到機器學習。macOS 提供了 arc4random
方法用于隨機數(shù)生成。不幸的是,這個方法無法在 Linux 上使用。并且,arc4random
方法只提供了 UInt32 類型的隨機數(shù)。事實上,/dev/urandom
這個設(shè)備文件中就提供了無限的隨機數(shù)。
這一小節(jié)中,我們將使用指針讀取這個文件,并產(chǎn)生完全類型安全的隨機數(shù)。
創(chuàng)建一個新 Playground
,命名為 RandomNumbers
,并確保選擇了 macOS 平臺。
創(chuàng)建完成后,添加如下代碼:
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)) } }
為了確保整個系統(tǒng)中只存在一個 file 變量,我們對其使用了 static
修飾符。系統(tǒng)會在我們的進程結(jié)束時關(guān)閉文件。因為我們有可能在多個線程中同時獲取隨機數(shù),所以需要使用一個串行的 GCD 隊列來進行保護。
get
函數(shù)是所有功能完成的地方。首先,我們根據(jù)傳入的大小分配了必要的內(nèi)存,注意這里需要 +1 是因為 fets
函數(shù)總是以 \0 結(jié)束。接下來,我們就使用 fgets
函數(shù)從文件中讀取數(shù)據(jù),確保我們在串行隊列中進行讀取操作。最后,我們先將數(shù)據(jù)封裝為一個 UnsafeMutableBufferPointer
,并將其轉(zhuǎn)化為一個數(shù)組。
在 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.randomized UInt8.randomized Int16.randomized UInt16.randomized Int16.randomized UInt32.randomized Int64.randomized UInt64.randomized
這里我們?yōu)?Integer 協(xié)議添加了一個靜態(tài)屬性,并為其提供了默認實現(xiàn)。我們首先獲取了隨機數(shù),隨后我們將獲得字節(jié)數(shù)組重新綁定為所需要的類型,然后返回它的值。簡單!
就這樣,我們使用 unsafe Swift 實現(xiàn)了一個類型安全的隨機器生成方法。
在日常開發(fā)中,我們并不會接觸到很多直接操作內(nèi)存的情境。但是掌握它的操作,能讓我們在碰到類似代碼里更加從容。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。
相關(guān)文章
Swift實現(xiàn)Selection Sort選擇排序算法的實例講解
選擇排序是一種穩(wěn)定的排序算法,且實現(xiàn)代碼通常比冒泡排序要來的簡單,這里我們就來看一下Swift實現(xiàn)Selection Sort選擇排序的實例講解2016-07-07RxSwift發(fā)送及訂閱 Subjects、Variables代碼示例
這篇文章主要介紹了RxSwift發(fā)送及訂閱 Subjects、Variables代碼示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-12-12Objective-C和Swift的轉(zhuǎn)換速查手冊(推薦)
這篇文章主要給大家介紹了關(guān)于Objective-C和Swift的轉(zhuǎn)換速查手冊的相關(guān)資料,文中通過示例代碼介紹的非常詳細,非常推薦給大家參考學習使用,需要的朋友們下面隨著小編來一起學習學習不2018-06-06Swift編程中用以管理內(nèi)存的自動引用計數(shù)詳解
這篇文章主要介紹了Swift編程中用以管理內(nèi)存的自動引用計數(shù)詳解,是Swift入門學習中的基礎(chǔ)知識,需要的朋友可以參考下2015-11-11