任何語言的底層實(shí)現(xiàn),其實(shí)都離不開指針,大部分高級語言都會(huì)將指針的操作隱匿起來,比如在Swift中我們很少會(huì)直接使用指針。但是這并不意味著我們在Swift中不能使用指針了,只是使用起來會(huì)更加麻煩 — 高層建筑總能能帶來便利性,但是,這將犧牲它的靈活性 — 沒有什么比指針更加靈活了。
由于Swift它是一門嚴(yán)格安全的語言,如果在Swift的框架之下,我們在編程中使用到的內(nèi)存管理將由Swift自動(dòng)處理,而指針則游離在此之外,也就是說,我們需要自己去管理指針占有內(nèi)存的管理,這將回到更加古老的編程手段中,顯然,在Swift中使用指針并不便捷,同時(shí)也不安全。
Swift當(dāng)然考慮到了這一點(diǎn),于是增添了一系列和指針有關(guān)的類和結(jié)構(gòu),目的是讓我們更輕松的在Swift中使用指針。
Swift中有兩大類的指針: typed pointer指定類型指針,raw pointer未指定類型的指針。不管是typed pointer還是raw pointer,他們的幾個(gè)子類都是以unsafe開頭,這同時(shí)也在告知使用者,在Swift 中使用指針是不安全的。
一、typed point
typed point是指定類型的指針,我們在使用typed point的使用,會(huì)明確知道該指針指向的類型。它包含了一共四個(gè)子類型,他們分別是:
- UnsafePointer
- UnsafeMutablePointer
- UnsafeBufferPointer
- UnsafeMutableBufferPointer
typed point指定了類型,也即相當(dāng)于我們在C語言中類似的指明了類型指針。
1. UnsafePointer
UnsafePointer 的官方描述是 訪問特定類型數(shù)據(jù)的指針。意即這個(gè)指針的類型是已知。
在上一篇文章中了解到,內(nèi)存的三種狀態(tài):
- 未綁定類型同時(shí)未初始化值
- 綁定類型但是未初始化值
- 綁定類型同時(shí)初始化值
每一種狀態(tài)都必須要有特定的指針區(qū)表示。這里的UnsafePointer就是指向綁定類型同時(shí)初始化值內(nèi)存的。
另外,它相當(dāng)于C系列語言中的const對象,我們不能直接初始化UnsafePointer對象(當(dāng)然如果指定地址是可以的,但是我們在實(shí)際開發(fā)中,我們不太可能直接使用這個(gè)方法初始化一個(gè)指針。因?yàn)檫@樣初始化的指針很不透明,你必須要確定對應(yīng)的地址存放的數(shù)據(jù)類型是對應(yīng)的,不然得到的將是一個(gè)位置的值)。同時(shí)我們也不能修改UnsafePointer指向的值,因?yàn)檫@是一個(gè)常量指針!
// 通過地址初始化,也即是讓這個(gè)指針指向某個(gè)地址,因?yàn)槲覀儍H僅制定了指針的類型,并沒有給指針初始化一塊內(nèi)存,所以它指向一個(gè)位置的東西。
let usp = UnsafePointer<Int>.init(bitPattern: 20)
print(usp.pointee) // 這里的打印是沒有意義的
//通過另一個(gè)變量指針初始化一個(gè)`UnsafePointer`常量指針
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
usmp.pointee = 20
let usp2 = UnsafePointer<Int>.init(usmp)
print(usp2.pointee) // 20
我們可以通過 pointee 屬性發(fā)訪問指針的值。但是要注意,UnsafePointer并不能對值進(jìn)行設(shè)置,如果需要可以設(shè)置指向值的指針,應(yīng)該使用UnsafeMutablePointer。
當(dāng)然我們還可以直接打印指針,得到這個(gè)指針指向的地址。
print(usp2) // 0x0000000133d08340
2. UnsafeMutablePointer
上面已經(jīng)做了一些對UnsafeMutablePointer的描述,它相當(dāng)于C系列語言中的變量指針。 :
C語言中:
int *p = malloc(sizeof(int));
Swift中創(chuàng)建一個(gè)UnsafeMutablePointer:
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
兩者的意義相同。
我們同樣可以通過 pointee 屬性發(fā)訪問指針的值。同時(shí),你還可以直接修改 pointee的值,就像我們使用Swift的其他類一樣。
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
usmp.pointee = 100
print(usmp.pointee) // 100
3. UnsafeBufferPointer
UnsafeBufferPointer指代一系列連續(xù)的內(nèi)存。我們可以使用這個(gè)指針作為一個(gè)序列的指針,并通過指針直接訪問旗下元素。
let intArr = [10,20,30,40,50]
let usbp = UnsafeBufferPointer.init(start: intArr, count: 4) // 截取了四個(gè)元素
print(usbp[0],usbp[1],usbp.count,usbp.last ?? "0")
usbp.forEach { (a) in
print(a)
}
//
10 20 4 Optional(40)
10
20
30
40
盡管這是一個(gè)Swift 的指針,但是我們依然像使用對象一樣操作了數(shù)組,因?yàn)閁nsafeBufferPointer 遵循了Collection 協(xié)議,說明這是一個(gè)可變的集合,對于集合我們進(jìn)行遍歷則無可厚非了。
但是同樣的,這個(gè)UnsafeBufferPointer是常量,它只能獲取到數(shù)據(jù),但是通過這個(gè)指針去修改數(shù)據(jù)。與之對應(yīng)的是UnsafeMutableBufferPointer指針。
4. UnsafeMutableBufferPointer
可變的系列指針。UnsafeMutableBufferPointer擁有對指向序列修改的能力:
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
let usmbp = UnsafeMutableBufferPointer<Int>.init(start: usmp, count: 5) // 拓展為5各元素的大小
usmbp[0] = 120
usmbp[1] = 130 //進(jìn)行修改 其他未修改的內(nèi)容將產(chǎn)生隨機(jī)值
usmbp.forEach { (a) in
print(a)
}
print(usmbp.count)
狀況跟UnsafeBufferPointer有點(diǎn)類似,只是在初始化的時(shí)候,需要借助UnsafeMutablePointer。 并不能直接使用已經(jīng)存在序列進(jìn)行初始化。
值的注意的是:如果一個(gè)序列被初始化之后,沒有給每一個(gè)元素賦值的話,這些元素的值都將出現(xiàn)隨機(jī)值。
二、raw pointer
raw pointer指未知類型的指針,這個(gè)類型相當(dāng)于C語言中的(void *)類型,。
同樣的,raw pointer也包含了四個(gè)子類型:
- UnsafeRawPointer
- UnsafeMutableRawPointer
- UnsafeBufferRawPointer
- UnsafeMutableBufferRawPointer
1. UnsafeRawPointer和UnsafeMutableRawPointer
UnsafeRawPointer類型不提供自動(dòng)內(nèi)存管理,也不保證其內(nèi)存,同時(shí)沒有做任何的內(nèi)存對齊。 開發(fā)者在使用UnsafeRawPointer的時(shí)候,應(yīng)該手動(dòng)對指針做內(nèi)存管理,以避免泄漏或未定義的行為。這一點(diǎn)和UnsafePointer類似,但是相對來說UnsafeRawPointer更加靈活,我們可以單純的創(chuàng)建一個(gè)指針,而不管它的類型和值。當(dāng)需要使用來操作某個(gè)內(nèi)存的時(shí)候,再來綁定類型和賦值。
和UnsafePointer一樣,UnsafeRawPointer同樣不能直接創(chuàng)建,我們需要借助的它的可變形態(tài)UnsafeMutableRawPointer來創(chuàng)建。
UnsafeMutableRawPointer 是 UnsafeRawPointer的可變版本,但是它依然沒有任何類型,也不確定任何值,我們創(chuàng)建的時(shí)候,需要給它指定占用內(nèi)存的大小和對齊方式,這樣就能形成一個(gè)完成的內(nèi)存布局:
let usmrp = UnsafeMutableRawPointer.allocate(bytes: 4, alignedTo: 1)
接下來通過創(chuàng)建的 usmrp創(chuàng)建一個(gè)UnsafeRawPointer的不可變的指針:
let usrp = UnsafeRawPointer.init(usmrp)
print(usmrp)
print(usrp)
// 同一塊內(nèi)存
0x0000000101e1a570
0x0000000101e1a570
我們在一開始就說了,raw pointer 沒有指定內(nèi)存的類型,也沒有初始化的值,他只是圈了一塊內(nèi)存,所以我們壓根就不知道它指向的那個(gè)內(nèi)存中到有什么。并且,它也不包含pointee這個(gè)屬性了。
雖然我們不知道raw pointer指向的內(nèi)存中有什么,但是我們可以通過raw pointer的 advanced(by:)函數(shù)獲取之后的地址的塊區(qū)。每一次調(diào)用advanced(by:)都會(huì)返回一個(gè)地址。如果將之后的地址綁定某個(gè)類型之后,我們就可以在這個(gè)地址的片區(qū)中賦值了。
let newPtr = usrp.advanced(by: 16) // 這是往后偏移 16個(gè)字節(jié)之后的地址
需要進(jìn)一步使用usrp和usmrp,應(yīng)該給他們綁定類型。 如果我們需要綁定類型,可以使用Swift提供的兩個(gè)函數(shù):
public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafeMutablePointer<T> // 綁定一個(gè)類型,以及類型的大小
public func assumingMemoryBound<T>(to: T.Type) -> UnsafeMutablePointer<T>
// 重綁定一個(gè)類型
我們可以通過上述的兩個(gè)函數(shù)來給一個(gè)指定的地址區(qū)域賦值:
newPtr. assumingMemoryBound(to: Int.self) // 將內(nèi)存綁定為 Int 類型
newPtr.initialize(12) //初始化賦值
print(newPtr.pointee) // 12
這里可以看到,我并沒有顯示的指定需要的賦值的哪部分內(nèi)存有多大,是因?yàn)楫?dāng)我們給內(nèi)存綁定了類型的時(shí)候,系統(tǒng)會(huì)自動(dòng)計(jì)算我想要是使用的內(nèi)存區(qū)域大?。╯izeOf(Int))。
不管是UnsafeRawPointer還是UnsafeMutableRawPointer都有上面兩個(gè)方法,他們都會(huì)返回 typed pointer, 分別和UnsafePointer和UnsafeMutablePointer對應(yīng)。
經(jīng)過綁定的raw pointer 就變成了typed pointer,這時(shí)候的使用參考文中typed pointer的內(nèi)容即可。
2. UnsafeRawBufferPointer和UnsafeMutableRawBufferPointer
內(nèi)存中的每個(gè)字節(jié)都被視為一個(gè)獨(dú)立于內(nèi)存綁定類型的 UInt8 值。 UnsafeRawBufferPointer和UnsafeMutableRawBufferPointer 指代的是一系列的沒有被綁定類型的內(nèi)存區(qū)域。我們可以理解成他們實(shí)際上就是一些數(shù)組,再綁定內(nèi)存之前,其中包含的元素則是每一個(gè)字節(jié)。
在底層,基本數(shù)據(jù)單元的復(fù)制是有效的,另外沒有被 retain 和 stong 的也是能夠安全的復(fù)制的,同樣的,對于來自C API的對象也能夠安全的復(fù)制。對于原聲的Swift類型,有的包含了引用的對象的復(fù)制則有可能失敗,但是我們可以使用指針對他們的值進(jìn)行復(fù)制,這樣的結(jié)果是有效的。如果我們強(qiáng)行對一下發(fā)類型進(jìn)行復(fù)制,不一定有效,除非使用像C語言中的APImemmove().來操作。
UnsafeRawBufferPointer和UnsafeMutableRawBufferPointer是內(nèi)存視圖,盡管我們知道它指向的內(nèi)存區(qū)域,但是它并不擁有這塊內(nèi)存的引用。復(fù)制UnsafeRawBufferPointer 類型的變量不會(huì)復(fù)制它的內(nèi)存;但是初始化一個(gè)集合到另一個(gè)新的集合過程會(huì)復(fù)制集合中的引用內(nèi)存。
代碼實(shí)現(xiàn)一下如何創(chuàng)建UnsafeRawBufferPointer和UnsafeMutableRawBufferPointer:
let usmrbp = UnsafeMutableRawBufferPointer.allocate(count: 3)
創(chuàng)建一個(gè)無類型的UnsafeMutableRawBufferPointer指針。 之前說了,內(nèi)存中每個(gè)字節(jié)都被視為一個(gè)獨(dú)立于內(nèi)存綁定類型的 UInt8 值,因此,這里的無類型實(shí)際上是有類型的,他應(yīng)該屬于 UInt8 類型。 而這個(gè)系列的大小應(yīng)該為 3 * MemoryLayout<T>.stride 。
我們可以向該指針中的內(nèi)存中賦值一個(gè)遵循 collect 的對象:
usmrbp.copyBytes(from: [5,56,6])
usmrbp.forEach { (a) in
print(a)
}
//
5
56
6
基本上, typed point描述的是我們已經(jīng)知道的哪部分內(nèi)存,通過typed point我們可以知道內(nèi)存的地址,內(nèi)存的類型,以及內(nèi)存中存儲(chǔ)的是什么。raw pointer 則描述的是一個(gè)內(nèi)存的起始地址值,通過這個(gè)起始的地址,我們能夠逐步的獲取到后續(xù)的地址值,結(jié)合綁定類型之后, typed point總能在使用一塊合適地址保存我們想要保存的值。
不管是raw pointer還是typed point指針,兩者都是不安全的,不安全的意義包含了內(nèi)存的管理和對其他未知內(nèi)存的侵害。 有時(shí)候使用指針無意改變了某些系統(tǒng)內(nèi)部的關(guān)鍵變量,將會(huì)導(dǎo)致莫名其妙的崩潰。
三、如何正確的使用raw pointer和typed point
我們自己創(chuàng)建的指針缺乏對內(nèi)存的管理,因此非常脆弱,如何正確安全的使用指針值得我們探究。Swift 提供了好幾種方式供我們用于指針和Swift原生類的交互。
1. Unmanaged 托管
因?yàn)橹苯邮褂弥羔樞枰覀內(nèi)ス芾韮?nèi)存,這很繁瑣,并且很危險(xiǎn)。于是,Unmanaged 出現(xiàn)了。Unmanaged 能夠?qū)⒂?C API 傳遞過來的指針進(jìn)行托管,我們可以通過Unmanaged標(biāo)定它是否接受引用計(jì)數(shù)的分配,以便實(shí)現(xiàn)類似自動(dòng)釋放的效果;同時(shí),如果不是使用引用計(jì)數(shù),也可以使用Unmanaged 提供的release函數(shù)來手動(dòng)釋放,這比在指針中進(jìn)行這些操作要簡單很多。
首先,假設(shè)我有一個(gè) PersonClass 類型的對象,我想將它用一個(gè)指針表示:
class PersonClass{
let name: String = "hua"
let age: Int = 22
}
let um = Unmanaged.passUnretained(PersonClass() as AnyObject).toOpaque()
let tpeUns = um.bindMemory(to: Byte.self, capacity: MemoryLayout<Int>.stride)
print(UnsafeMutablePointer<Byte>(tpeUns))
//
0x0000000153e490e0
打印的地址則是這個(gè)對象的存儲(chǔ)地址了,我們可以使用這個(gè)指針來表示這個(gè)對象。如果我們想取出和更改這個(gè)對象中的屬性 name 和 age 的時(shí)候,可以這樣操作:
let rptr = UnsafeMutableRawPointer(tpeUns) // 轉(zhuǎn)化成不透明指針, 用于重綁定類型。
// 注意,再上一篇文章中已經(jīng)介紹,Swift 中類的首地址有效值在 16 字節(jié)之后。
let namePtr = rptr.advanced(by: 16).assumingMemoryBound(to: String.self)
let agePtr = rptr.advanced(by: MemoryLayout<String>.stride + 16).assumingMemoryBound(to: Int.self)
print(namePtr.pointee,agePtr.pointee) // 取出內(nèi)存值
print(ps.name,ps.age) // 打印原對象
//namePtr.pointee = "Tom - Loo1" //直接給指針賦值也是可以的。
namePtr.initialize(to: "Tom - Loo") // 重新初始化內(nèi)存中的值
agePtr.initialize(to: 26)
print(ps.name,ps.age)
// 打印
Tom 22
Tom - Loo 26
總結(jié)起來,基本思路就是獲取到了對象的地址(即首地址,注意這個(gè)地址應(yīng)該使用不透明指針表示,以便后續(xù)根據(jù)屬性的不同類型重新綁定),然后偏移16個(gè)字節(jié),跳過 meta 信息所占的空間。根據(jù)屬性的排布順序,逐步計(jì)算取出每個(gè)屬性所對應(yīng)的指針,有了每個(gè)屬性的指針,要賦值就簡單了。 令人驚喜的是: 這里不管是let 屬性還是 var 屬性,都可以進(jìn)行賦值的。
其實(shí)到這里,如何進(jìn)行內(nèi)存的基本思路已經(jīng)出來了。 但是這篇文章主要是介紹指針的使用,我們繼續(xù)看看另一種指針使用方式,用來獲取一個(gè)屬性而非整個(gè)對象的指針。
2. withPoint系列方法
Swift提供了一種更為簡便的方式給我們獲取一個(gè)屬性的指針,那就是withPoint系列方法:
public func withUnsafeMutablePointer<T, Result>(to arg: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
public func withUnsafePointer<T, Result>(to arg: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
public func withExtendedLifetime<T, Result>(_ x: T, _ body: (T) throws -> Result) rethrows -> Result
public func withExtendedLifetime<T, Result>(_ x: T, _ body: () throws -> Result) rethrows -> Result
一般使用前面兩個(gè)函數(shù)的概率更高,我重點(diǎn)說下這兩個(gè)。
同樣對于上述的PersonClass類,如果修改它的屬性值,我們可以如下操作:
withUnsafeMutablePointer(to: &ps.name) {
print($0.pointee)
$0.pointee = "Tom - hans"
}
print(ps.name)
這個(gè)函數(shù)如果使用在這里實(shí)際上并沒有什么優(yōu)勢: 如果使用withUnsafeMutablePointer(to:) 方法修改屬性值,必須保證屬性的類型是 var ,雖然在內(nèi)部我們可以使用初始化指針的方式強(qiáng)行改變,但是在調(diào)用方法時(shí),編譯器會(huì)提示需要加入一個(gè) var 的變量,而不能是常量。 如此說,我們?yōu)槭裁床恢苯邮褂脤傩缘狞c(diǎn)語法直接賦值呢?
本文對Swift的中指針介紹只是它的一部分功能,筆者能力有限,可能在文中出現(xiàn)紕漏和錯(cuò)誤。歡迎指正。