Swift中的指針

引言

通常來講,Swift是一門很是安全的語言,平時作業務的時候,基本上用的結構體,類等,都不會直接操做內存,並且有可選值的包裝,也保證了值的安全。可是也有一些比較特殊的業務須要用到指針,好比與底層C的交互,字節流的解析等。這時候,Swift將會變得沒那麼安全,須要咱們當心翼翼的操做,而且須要深入理解Swift的指針類型swift

MemoryLayout

在講指針以前,必須先得了解一下Swift的內存佈局MemoryLayout,不一樣的基礎類型、結構體、枚舉、類等等,他們在內存中佔有的大小,步長,對齊長度都不同。一旦指針的偏移量、獲取值的長度錯誤,輕則獲取的值錯誤,重則程序崩潰。數組

基礎類型的MemoryLayout

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是一個在編譯時評估的泛型類型。它肯定每一個指定類型的大小、對齊方式和步長,並返回一個以字節爲單位的數字。安全

咱們能夠靠如上的方式獲取數據類型的MemoryLayoutmarkdown

  • size:數據類型的長度,就是數據在內存中佔據的大小
  • alignment:數據類型的對齊方式,在某些結構中(好比在Raw Pointer),數據在內存的首地址必須是alignment的倍數,不然將會崩潰,例如Int16alignment是2,那麼Int16在內存的首地址必須是偶數,不然奔潰
  • stride:數據類型的步長,應該是alignment的倍數,若是是一串緊挨的數據,那麼下一個數據會在大於等於stride的地址以後

關於數據類型的對齊方式舉個例子,Demo以下:ide

//向堆申請開闢空間
let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)
//結束時釋放空間
defer {
    p.deallocate()
}
//在這塊連續的內存空間內,放上0,1,2,3
for i in 0..<4 {
    p.advanced(by: i * 8).storeBytes(of: i, as: Int.self)
}
let offset = 8
print(p.advanced(by: offset).load(as: Int.self))
複製代碼

Intalignment爲8,當offset爲8的倍數時,能夠取到對應的值,即便越界,也能取到0或者奇怪的數字,不會崩潰。但若是offset不爲8的倍數時,程序直接飄紅: 函數

將會出現:oop

Fatal error: load from misaligned raw pointer佈局

翻譯過來就是:post

致命錯誤:從未對齊的原始指針加載ui

其餘類型的MemoryLayout

Swift中還有許多其餘的類型:結構體,枚舉,類等

咱們能夠用一樣的方式來查看他們的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

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 = 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)
複製代碼

咱們能夠發現:

EmptyStruct空結構體的大小爲零。對齊大小是1,全部數字均可以被1整除,因此它能夠存放於任何地址。步長也爲1,雖然它的大小爲零,可是必須有一個惟一的內存地址。

SampleStruct的大小爲5,等於UInt32Bool大小的相加。對齊大小是4,等於UInt32的對齊大小,屬性中最大的對齊方式會決定結構體的對齊方式。步長爲8,比5大的,最小的4的倍數爲8,因此步長爲8。

EmptyClassSampleClass,無論是否包含屬性,他們的大小,對齊方式,步長都爲8,由於類是引用類型,本質上就是一個指針,在64位的系統下,指針長度就是爲8.

固然,還有枚舉、數組、字典等等的MemoryLayout,以及結構體屬性內部順序不同,會形成結構體的大小也不同,這裏就不詳細說明了,感興趣的能夠本身實驗下,有時間我會單獨開一篇文章詳述。

指針類型

Swift中,由於指針能夠直接操做內存,因此用上了Unsafe的開頭修飾,雖然每次申明指針的時候,要多寫這個修飾詞,可是能夠時刻提醒着你在訪問編譯器沒有檢查的內存,若是操做不當,你會遇到一些奇怪的問題,甚至奔潰。

Swift不像Cchar *那樣只提供一種非結構化方式訪問內存的非安全指針類型。Swift包含近12種指針類型,每種類型都有不一樣的功能和用途。

咱們看下經常使用的8個指針類型:

指針類型 Editable Collection Strideable Typed
UnsafeMutablePointer<T> YES NO YES YES
UnsafePointer<T> NO NO YES YES
UnsafeMutableBufferPointer<T> YES YES NO YES
UnsafeBufferPointer<T> NO YES NO YES
UnsafeMutableRawPointer YES NO YES NO
UnsafeRawPointer NO NO YES NO
UnsafeMutableRawBufferPointer YES YES NO NO
UnsafeRawBufferPointer NO YES NO NO
  • Editable: 類型名中帶有Mutable的,說明都是能夠寫入更改數據的,其他的只能讀取,並不能修改,能夠保護數據安全。根據本身的需求看,是否須要Mutable
  • Collection: 類型名中帶有Buffer的,都具備Collection的特性,若是查看他們的聲明文件,發現他們都遵照Collection協議
  • Strideable: 不帶有Buffer的能夠調用advanced方法,Typed類型的默認步長爲T的步長,而Raw類型的默認步長爲1
  • Typed: 有範型的是Typed Pointer,帶有Raw字段的是Raw Pointer

不一樣的類型會用在不一樣的場景,看狀況使用,或者相互轉換

指針的操做

咱們能夠在堆上開闢一塊空間,而後用指針指向它。在調用初始化方法的時候,指針必須是Mutable類型的,否則沒有意義。並且指針是Unsafe的,編譯器不會參與該指針的生命週期,因此,本身在堆上申請空間後,結束使用時必需要本身釋放,否則會內存泄露。

Unsafe Pointer初始化

//像堆內存申請開闢空間
var mutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
//做用域結束時執行
defer {
    //釋放開闢的空間
    mutableRawPointer.deallocate()
}

var mutableTypedPointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
mutableTypedPointer.initialize(repeating: 0, count: count)
defer {
    mutableTypedPointer.deinitialize(count: count)
    mutableTypedPointer.deallocate()
}

var mutableRawBufferPointer = UnsafeMutableRawBufferPointer.allocate(byteCount: byteCount, alignment: alignment)
defer {
    mutableRawBufferPointer.deallocate()
}

var mutableTypedBufferPointer = UnsafeMutableBufferPointer<Int>.allocate(capacity: count)
mutableTypedBufferPointer.initialize(repeating: 0)
defer {
    mutableTypedBufferPointer.deallocate()
}
複製代碼

Unsafe Pointer賦值與取值操做

//mutableRawPointer的存值與取值,mutableRawPointer默認步長爲1
(mutableRawPointer + 2 * alignment).storeBytes(of: 23, as: Int.self)
print(mutableRawPointer.advanced(by: 2 * alignment).load(as: Int.self))

mutableRawPointer.storeBytes(of: 33, toByteOffset: 2 * alignment, as: Int.self)
print(mutableRawPointer.load(fromByteOffset: 2 * alignment, as: Int.self))

//mutableTypedPointer的存值與取值,mutableRawPointer默認步長爲Int的步長
mutableTypedPointer.advanced(by: 2).pointee = 27
print(mutableTypedPointer[2])

(mutableTypedPointer + 2).pointee = 37
print(mutableTypedPointer.successor().successor().pointee)

//mutableRawBufferPointer的存值與取值,能夠像數組同樣取單個元素,不過是UInt8的
mutableRawBufferPointer.storeBytes(of: 13, toByteOffset: 2 * alignment, as: Int.self)
print(mutableRawBufferPointer.load(fromByteOffset: 2 * alignment, as: Int.self))
//發現內存中有垃圾值,可是找不到初始化成0的便利方法,好奇怪
for (index, value) in mutableRawBufferPointer.enumerated() {
    print("value \(index): \(value)")
}

//mutableTypedBufferPointer的存值與取值,和mutableRawBufferPointer當作一個數組,不過mutableRawBufferPointer是[UInt8],而mutableTypedBufferPointer是[Int]
mutableTypedBufferPointer[2] = 9
print(mutableTypedBufferPointer[2])
for (index, value) in mutableTypedBufferPointer.enumerated() {
    print("value \(index): \(value)")
}
複製代碼

同類型mutable互轉

var rawPointer = UnsafeRawPointer(mutableRawPointer)
var typedPointer = UnsafePointer(mutableTypedPointer)
var rawBufferPointer = UnsafeRawBufferPointer(mutableRawBufferPointer)
var typedBufferPointer = UnsafeBufferPointer(mutableTypedBufferPointer)

mutableRawPointer = UnsafeMutableRawPointer(mutating: rawPointer)
mutableTypedPointer = UnsafeMutablePointer(mutating: typedPointer)
mutableRawBufferPointer = UnsafeMutableRawBufferPointer(mutating: rawBufferPointer)
mutableTypedBufferPointer = UnsafeMutableBufferPointer(mutating: typedBufferPointer)
複製代碼

在OC中的可變到不可變,每每意味着深拷貝。那麼在這裏的指針會不會拷貝一分內容到新的指針麼?

咱們能夠打印下他們的地址:

咱們能夠很明顯看到,他們的地址徹底沒變,換句話說,他們都指向同一片內存地址。因此mutable這個修飾詞只是在API層面限制你能不能修改內存而已。

不一樣類型的指針相互轉化

/** 這塊轉化會引發上方defer中釋放的崩潰,由於對象變了,同一塊空間屢次釋放了,這邊只是示範轉化,真的要運行程序,請註釋這塊 */

//其餘類型轉mutableRawPointer
mutableRawPointer = UnsafeMutableRawPointer(mutableTypedPointer)
//若是你能保證count不爲0,能夠強轉。可是若是baseAddress有值,不必定count大於0。在底層,UnsafePointer<T> 和 Optional<UnsafePointer<T> 的內存結構徹底相同;編譯器會將 Optional.none 映射爲一個全部位全爲零的空指針。
mutableRawPointer = mutableRawBufferPointer.baseAddress!
mutableRawPointer = UnsafeMutableRawPointer(mutableTypedBufferPointer.baseAddress!)

//其餘類型轉mutableTypedPointer
mutableTypedPointer = mutableRawPointer.bindMemory(to: Int.self, capacity: count)
mutableTypedPointer = mutableRawPointer.assumingMemoryBound(to: Int.self)
mutableTypedPointer = mutableRawBufferPointer.baseAddress!.assumingMemoryBound(to: Int.self)
mutableTypedPointer = mutableTypedBufferPointer.baseAddress!

//其餘類型轉mutableRawBufferPointer
mutableRawBufferPointer = UnsafeMutableRawBufferPointer.init(start: mutableRawPointer, count: byteCount)
mutableRawBufferPointer = UnsafeMutableRawBufferPointer.init(start: UnsafeMutableRawPointer(mutableTypedPointer), count: byteCount)
mutableRawBufferPointer = UnsafeMutableRawBufferPointer(mutableTypedBufferPointer)

//其餘類型轉mutableTypedBufferPointer
mutableTypedBufferPointer = UnsafeMutableBufferPointer.init(start: mutableRawPointer.assumingMemoryBound(to: Int.self), count: count)
mutableTypedBufferPointer = UnsafeMutableBufferPointer.init(start: mutableTypedPointer, count: count)
mutableTypedBufferPointer = mutableRawBufferPointer.bindMemory(to: Int.self)
mutableTypedBufferPointer = UnsafeMutableBufferPointer.init(start: mutableRawBufferPointer.baseAddress?.assumingMemoryBound(to: Int.self), count: mutableRawBufferPointer.count/MemoryLayout<Int>.stride)

//終極轉化方法(buffer不建議用,至少count就不同),比較危險,除非確保正確,類的引用也能夠靠這個變成指針
mutableRawPointer = unsafeBitCast(mutableTypedPointer, to: UnsafeMutableRawPointer.self)
mutableTypedPointer = unsafeBitCast(mutableRawPointer, to: UnsafeMutablePointer<Int>.self)
複製代碼

這邊的轉化方法應該不全,若是好的方法遺漏的,歡迎評論區補充。

bindMemoryassumingMemoryBound的區別

先貼下官方註釋 bindMemory:

Binds the memory to the specified type and returns a typed pointer to the bound memory.

Use the bindMemory(to:capacity:) method to bind the memory referenced by this pointer to the type T. The memory must be uninitialized or initialized to a type that is layout compatible with T. If the memory is uninitialized, it is still uninitialized after being bound to T.

assumingMemoryBound:

Returns a typed pointer to the memory referenced by this pointer, assuming that the memory is already bound to the specified type.

Use this method when you have a raw pointer to memory that has already been bound to the specified type. The memory starting at this pointer must be bound to the type T. Accessing memory through the returned pointer is undefined if the memory has not been bound to T. To bind memory to T, use bindMemory(to:capacity:) instead of this method.

這裏先說下幾個狀態

  • Uninitialised raw memory: 未初始化的原始內存
  • Uninitialised memory that's bound to a type: 綁定到類型的未初始化內存
  • Initialised memory bound to a type: 綁定到類型的初始化內存

當你使用UnsafeMutableRawPointer.allocate(bytes:alignment:)分配內存時,你獲得的是未初始化的原始內存。

當你使用UnsafeMutablePointer<T>.allocate(capacity:)分配內存時,你會獲得綁定到類型T的未初始化內存。

當你使用上述指針調用initialize系列方法能夠獲得綁定到類型T的初始化內存

若是指針的Pointee類型 (也就是指針指向的數據類型) 是一個須要內存管理的非簡單類型 (例如在一個類或結構體中包含了其它類),你必須在調用deallocate以前調用deinitializeinitializedeinitialize方法用於管理ARC中的引用計數。忘記調用deinitialize 可能會引發內存泄漏。更糟的是,若是忘記調用initialize,例如直接用下標操做符給指向一片未初始化內存的指針賦值,能夠引起各類未定義的問題甚至讓程序崩潰。

若是指針的Pointee類型是一個簡單類型(例如結構體,枚舉等),能夠清除內存中的垃圾值,以避免引發不可預料的錯誤,這個和C初始化內存很像。

咱們看了官方的解釋,能夠簡單理解爲:

  • 若是你的這塊內存是未初始化的原始內存,調用bindMemory後,將會獲得綁定到類型的未初始化內存
  • 若是你的這塊內存是綁定到類型的初始化內存,但你想要綁定的類型和已綁定的類型的佈局兼容,那麼調用bindMemory後,將會獲得新綁定到類型的初始化內存
  • bindMemory只能上述兩種狀況下使用,其餘狀況能夠調用assumingMemoryBound(包括上述的兩種狀況也能夠調用,不過在返回未初始化的原始內存時,會出現未定義的指針,= =不懂啥意思,哈哈,評論區請指教)。

這裏區分下相關類型和佈局兼容類型是獨立的概念:

  • 佈局兼容:若是綁定到類型U的內存能夠按位從新解釋爲具備類型T,則類型T在佈局上與類型U兼容。注意,這並不必定是一種雙向關係。例如,若是講元祖(Int, Int)的一個'實例'能夠被從新解釋爲2*Int,則Int(Int, Int)佈局兼容。但你不能用一個Int來構成一個(Int, Int)值。
  • 相關類型:若是能夠用這兩種類型爲重疊內存別名,那麼這兩種類型是相關的。例如,若是你有一個UnsafePointer和一個UnsafePointer,若是T和U是不相關的類型,那麼它們就不能指向彼此重疊的內存。

咱們看下源碼的區別

@_transparent
@discardableResult
public func bindMemory<T>( to type: T.Type, capacity count: Int ) -> UnsafePointer<T> {
    Builtin.bindMemory(_rawValue, count._builtinWordValue, type)
    return UnsafePointer<T>(_rawValue)
}

@_transparent
public func assumingMemoryBound<T>(to: T.Type) -> UnsafePointer<T> {
    return UnsafePointer<T>(_rawValue)
}
複製代碼
  • @_transparent:和@inline(__always)很是相似,能夠理解成內聯函數,編譯的時候,直接將函數體拷貝到調用的地方,有點像宏定義,這樣不用開闢新的棧,效率提升了。
  • @discardableResult:顧名思義,函數的返回結果能夠不使用。若是不加這個關鍵詞,那麼你不使用該返回值時,編譯器會飄黃提醒。
  • Builtin:是內置命令,能夠先了解下Swift編譯過程BuiltinLLVM IR的類型和方法直接暴露給Swift標準庫,Builtin模塊只有在標準庫內部才能夠訪問。

咱們能夠看到bindMemoryassumingMemoryBound只多了調用一個Builtin.bindMemory(_rawValue, count._builtinWordValue, type),猜想底層作了某些操做(也許沒有哈),下次有機會分析LLVM源碼,這塊補上。bindMemory有個@discardableResult詞修飾,說明這個方法更傾向於綁定類型,並不關注結果,只是順帶返回給你。而assumingMemoryBound真的只是從API層面將一段內存空間,變成你想綁定的類型操做。我的推薦,除非你真正想綁定內存類型,不然仍是assumingMemoryBound用的狀況多一點。

不過我的目前測下來,二者方法暫時沒有區別。。。若是大佬有知道的,請告知。我查到資料在Swift3中的確是沒有區別的,當時多是ABI穩定的問題,如今就不知道了。不過仍是建議遵從官方的使用方法,誰知道之後會怎麼樣呢,也許更新後就改了呢,是吧。

Swift中的常見類型轉指針

結構體

結構體是Swift的基礎類型,像IntDoubleBool等,都是結構體。下面舉個列子:

struct Teacher {
    var age = 12
    var name = "Tom"
}

var tc = Teacher.init()
withUnsafePointer(to: &tc) { pointer in
    // let pointer: UnsafePointer<Teacher>
    print("name: \(pointer.pointee.name), age: \(pointer.pointee.age)")
    // name: Tom, age: 12
}

// 能夠不加取地址符號,效果同樣
withUnsafeMutablePointer(to: tc) { pointer in
    // let pointer: UnsafeMutablePointer<Teacher>
    pointer.pointee.age = 15
    pointer.pointee.name = "Harry"
    print("name: \(pointer.pointee.name), age: \(pointer.pointee.age)")
    // name: Harry, age: 15
}

withUnsafeBytes(of: &tc) { pointer in
    // let pointer: UnsafeRawBufferPointer
    print("data count: \(pointer.count)")
    // data count: 24
    print("name: \(pointer.load(as: Int.self)), age: \(pointer.load(fromByteOffset: MemoryLayout<Int>.stride, as: String.self))")
    // name: 15, age: Harry 能夠看到這裏的值被上面已經改掉了,印證操做的同一片內存空間
}

withUnsafeMutableBytes(of: &tc) { pointer in
    // let pointer: UnsafeMutableRawBufferPointer
    pointer.storeBytes(of: 18, as: Int.self)
    pointer.storeBytes(of: "Marry", toByteOffset: MemoryLayout<Int>.stride, as: String.self)
    print("data count: \(pointer.count)")
    // data count: 24
    print("name: \(pointer.load(as: Int.self)), age: \(pointer.load(fromByteOffset: MemoryLayout<Int>.stride, as: String.self))")
    // name: 18, age: Marry
}
複製代碼

系統提供了4種方法獲取值類型的指針,根據本身須要,選擇合適的方法,若是你不想改變實例的值,建議不要使用mutable的方法

類指針

類是引用類型,自己是一個指針,咱們能夠轉成一個UnsafeRawPointer指針,但咱們分析過類的結構,那咱們能夠聲明一個大體的結構體HeapObject,將類轉成UnsafePointer<HeapObject>類型,下面看下Demo:

struct HeapObject {
    var kind: Int
    var unownedRef: UInt32
    var strongref: UInt32
}

class Teacher {
    var age = 18
}

var t = Teacher()
// 咱們能夠不斷聲明變量使strongref增長
var t1 = t

do { // 可使用Unmanaged獲取指針,let ptr: UnsafeMutableRawPointer
    let ptr = Unmanaged.passUnretained(t).toOpaque()
    print(ptr.assumingMemoryBound(to: HeapObject.self).pointee)
    // HeapObject(kind: 4295000432, unownedRef: 3, strongref: 2)
}

do { // 直接強轉,由於咱們肯定結構是一致的
    let ptr = unsafeBitCast(t, to: UnsafePointer<HeapObject>.self)
    print(ptr.pointee)
    // HeapObject(kind: 4295000432, unownedRef: 3, strongref: 2)
}
複製代碼

不能使用withUnsafePointer方法獲取類的指向,由於你無論用不用取地址符,都拿到的是指針的指針(多態方法作處理了),並非咱們想要的。因此能夠採用上面兩種方法獲取指針。

其他不常見的指針

由於很不常見,因此就一筆帶過了

  • OpaquePointer:一個不透明的C指針的包裝,用於表示不能用Swift表示的類型的C指針,例如不完整的struct類型。
  • ManagedBufferPointer:包含一個buffer對象,並提供對Header實例,以及對存儲在該緩衝區中的任意數量的Element實例的連續存儲的訪問。在大多數狀況下,ManagedBuffer類能夠很好地知足這個目的,而且能夠單獨使用。然而,在不一樣類的對象必須用做存儲的狀況下,就須要ManagedBufferPointer
  • AutoreleasingUnsafeMutablePointer:一個可變指針,指向一個不擁有目標的Objective-C引用,Pointee必須是一個類類型或Optional<C>,其中C是一個類。該類型有隱式轉換,容許將如下任何一種傳遞給CObjC的API:
    • nil,它做爲一個空指針傳遞。
    • 引用類型的inout參數,它做爲一個指針傳遞給一個回寫臨時對象,該臨時對象具備自動釋放全部權語義。
    • 'UnsafeMutablePointer<Pointee>,按原樣傳遞。
  • CVaListPointer:空結構體,不知道啥做用。。

參考文獻

相關文章
相關標籤/搜索