通常來講,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是一個在編譯時評估的泛型類型。它肯定每一個指定類型的大小、對齊方式和步長,並返回一個以字節爲單位的數字。安全
咱們能夠靠如上的方式獲取數據類型的MemoryLayout
:markdown
size
:數據類型的長度,就是數據在內存中佔據的大小alignment
:數據類型的對齊方式,在某些結構中(好比在Raw Pointer
),數據在內存的首地址必須是alignment
的倍數,不然將會崩潰,例如Int16
的alignment
是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))
複製代碼
Int
的alignment
爲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,等於UInt32
和Bool
大小的相加。對齊大小是4,等於UInt32
的對齊大小,屬性中最大的對齊方式會決定結構體的對齊方式。步長爲8,比5大的,最小的4的倍數爲8,因此步長爲8。
EmptyClass
和SampleClass
,無論是否包含屬性,他們的大小,對齊方式,步長都爲8,由於類是引用類型,本質上就是一個指針,在64位的系統下,指針長度就是爲8.
固然,還有枚舉、數組、字典等等的MemoryLayout
,以及結構體屬性內部順序不同,會形成結構體的大小也不同,這裏就不詳細說明了,感興趣的能夠本身實驗下,有時間我會單獨開一篇文章詳述。
在Swift
中,由於指針能夠直接操做內存,因此用上了Unsafe
的開頭修飾,雖然每次申明指針的時候,要多寫這個修飾詞,可是能夠時刻提醒着你在訪問編譯器沒有檢查的內存,若是操做不當,你會遇到一些奇怪的問題,甚至奔潰。
Swift
不像C
的char *
那樣只提供一種非結構化方式訪問內存的非安全指針類型。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
類型的默認步長爲1Typed
: 有範型的是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)")
}
複製代碼
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)
複製代碼
這邊的轉化方法應該不全,若是好的方法遺漏的,歡迎評論區補充。
bindMemory
和assumingMemoryBound
的區別先貼下官方註釋 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 typeT
. The memory must be uninitialized or initialized to a type that is layout compatible withT
. If the memory is uninitialized, it is still uninitialized after being bound toT
.
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 toT
. To bind memory toT
, usebindMemory(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
以前調用deinitialize
。initialize
和deinitialize
方法用於管理ARC
中的引用計數。忘記調用deinitialize
可能會引發內存泄漏。更糟的是,若是忘記調用initialize
,例如直接用下標操做符給指向一片未初始化內存的指針賦值,能夠引起各類未定義的問題甚至讓程序崩潰。
若是指針的Pointee
類型是一個簡單類型(例如結構體,枚舉等),能夠清除內存中的垃圾值,以避免引發不可預料的錯誤,這個和C
初始化內存很像。
咱們看了官方的解釋,能夠簡單理解爲:
bindMemory
後,將會獲得綁定到類型的未初始化內存bindMemory
後,將會獲得新綁定到類型的初始化內存bindMemory
只能上述兩種狀況下使用,其餘狀況能夠調用assumingMemoryBound
(包括上述的兩種狀況也能夠調用,不過在返回未初始化的原始內存時,會出現未定義的指針,= =不懂啥意思,哈哈,評論區請指教)。這裏區分下相關類型和佈局兼容類型是獨立的概念:
U
的內存能夠按位從新解釋爲具備類型T
,則類型T
在佈局上與類型U
兼容。注意,這並不必定是一種雙向關係。例如,若是講元祖(Int, Int)
的一個'實例'能夠被從新解釋爲2*Int
,則Int
與(Int, Int)
佈局兼容。但你不能用一個Int
來構成一個(Int, Int)
值。咱們看下源碼的區別
@_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
的編譯過程,Builtin
將LLVM IR
的類型和方法直接暴露給Swift
標準庫,Builtin
模塊只有在標準庫內部才能夠訪問。咱們能夠看到bindMemory
比assumingMemoryBound
只多了調用一個Builtin.bindMemory(_rawValue, count._builtinWordValue, type)
,猜想底層作了某些操做(也許沒有哈),下次有機會分析LLVM
源碼,這塊補上。bindMemory
有個@discardableResult
詞修飾,說明這個方法更傾向於綁定類型,並不關注結果,只是順帶返回給你。而assumingMemoryBound
真的只是從API
層面將一段內存空間,變成你想綁定的類型操做。我的推薦,除非你真正想綁定內存類型,不然仍是assumingMemoryBound
用的狀況多一點。
不過我的目前測下來,二者方法暫時沒有區別。。。若是大佬有知道的,請告知。我查到資料在Swift3
中的確是沒有區別的,當時多是ABI穩定的問題,如今就不知道了。不過仍是建議遵從官方的使用方法,誰知道之後會怎麼樣呢,也許更新後就改了呢,是吧。
Swift
中的常見類型轉指針結構體是Swift
的基礎類型,像Int
、Double
、Bool
等,都是結構體。下面舉個列子:
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
是一個類。該類型有隱式轉換,容許將如下任何一種傳遞給C
或ObjC
的API:
nil
,它做爲一個空指針傳遞。inout
參數,它做爲一個指針傳遞給一個回寫臨時對象,該臨時對象具備自動釋放全部權語義。UnsafeMutablePointer<Pointee>
,按原樣傳遞。CVaListPointer
:空結構體,不知道啥做用。。