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