Swift內(nèi)存賦值探索二: 指針在Swift中的使用

任何語言的底層實(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. UnsafeRawPointerUnsafeMutableRawPointer
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)建。
UnsafeMutableRawPointerUnsafeRawPointer的可變版本,但是它依然沒有任何類型,也不確定任何值,我們創(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 pointeradvanced(by:)函數(shù)獲取之后的地址的塊區(qū)。每一次調(diào)用advanced(by:)都會(huì)返回一個(gè)地址。如果將之后的地址綁定某個(gè)類型之后,我們就可以在這個(gè)地址的片區(qū)中賦值了。

 let newPtr = usrp.advanced(by: 16)  // 這是往后偏移 16個(gè)字節(jié)之后的地址

需要進(jìn)一步使用usrpusmrp,應(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, 分別和UnsafePointerUnsafeMutablePointer對應(yīng)。
經(jīng)過綁定的raw pointer 就變成了typed pointer,這時(shí)候的使用參考文中typed pointer的內(nèi)容即可。

2. UnsafeRawBufferPointerUnsafeMutableRawBufferPointer
內(nèi)存中的每個(gè)字節(jié)都被視為一個(gè)獨(dú)立于內(nèi)存綁定類型的 UInt8 值。 UnsafeRawBufferPointerUnsafeMutableRawBufferPointer 指代的是一系列的沒有被綁定類型的內(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().來操作。
UnsafeRawBufferPointerUnsafeMutableRawBufferPointer是內(nèi)存視圖,盡管我們知道它指向的內(nèi)存區(qū)域,但是它并不擁有這塊內(nèi)存的引用。復(fù)制UnsafeRawBufferPointer 類型的變量不會(huì)復(fù)制它的內(nèi)存;但是初始化一個(gè)集合到另一個(gè)新的集合過程會(huì)復(fù)制集合中的引用內(nèi)存。
代碼實(shí)現(xiàn)一下如何創(chuàng)建UnsafeRawBufferPointerUnsafeMutableRawBufferPointer:

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 pointertyped 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è)對象中的屬性 nameage 的時(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ò)誤。歡迎指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容