Swift 指針

前言

本篇文章主要講解一下Swift中的指針,以及相關(guān)的應(yīng)用場景,指針也是面試官經(jīng)常問到的知識點,希望大家能夠掌握。

一、指針類別

Swift中的指針分為兩類

  1. typed pointer: 指定數(shù)據(jù)類型指針,即UnsafePointer<T>,其中T表示泛型
  2. raw pointer: 未指定數(shù)據(jù)類型的指針(原生指針) ,即UnsafeRawPointer

與OC中的指針的對比??

OC Swift 釋義
const T * unsafePointer<T> 指針及所指向的內(nèi)容都不可變
T * unsafeMutablePointer 指針及所指向的內(nèi)容都可變
const void * unsafeRawPointer 無類型指針,指向的值必須是常量
void * unsafeMutableRawPointer 無類型指針,也叫通用指針

1.1 type pointer

我們獲取基本數(shù)據(jù)類型地址可通過withUnsafePointer(to:)方法獲取,例如??

var age = 18
let p = withUnsafePointer(to: &age) { ptr in
    return ptr }
print(p)

在Swift源碼中搜索withUnsafePointer(to:),查看其定義??

我們以arm64為例,其定義??

@inlinable public func withUnsafePointer<T, Result>(to value: T, _ body: (Swift.UnsafePointer<T>) throws -> Result) rethrows -> Result {}

第二個參數(shù)傳入的是閉包表達式,然后通過rethrows重新拋出Result(即閉包表達式產(chǎn)生的結(jié)果)。既然是閉包,那么上面的例子,我們可以簡寫??

withUnsafePointer(to: &age){print($0)}

因為withUnsafePointer方法中的閉包屬于單一表達式,因此可以省略參數(shù)、返回值,直接使用$0,$0等價于ptr,表示第一個參數(shù),$1表示第二個參數(shù),以此類推。

再看看p的類型??

類型是UnsafePointer<Int>。

如何訪問指針指向的值

我們可以通過指針的pointee屬性訪問變量值??

var age = 18
let p = withUnsafePointer(to: &age) { ptr in
    return ptr }
print(p.pointee)
如何修改指針指向的值

有2種方式,間接修改 & 直接修改。

  • 間接修改
var age = 18
age = withUnsafePointer(to: &age) { ptr in
    //返回Int整型值
    return ptr.pointee + 12
}
print(age)

在閉包中直接通過ptr.pointee修改返回。

  • 直接修改
    直接修改也分2種方式??
  1. 通過withUnsafeMutablePointer方法??
var age = 18
withUnsafeMutablePointer(to: &age) { ptr in
    ptr.pointee += 12
}
  1. 通過allocate創(chuàng)建UnsafeMutablePointer??
var age = 18
//分配容量大小,為8字節(jié)
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//初始化
ptr.initialize(to: age)
ptr.deinitialize(count: 1)

ptr.pointee += 12
print(ptr.pointee)

//釋放
ptr.deallocate()

通過allocate創(chuàng)建UnsafeMutablePointer,需要注意以下幾點??

  • initializedeinitialize需成對使用
  • deinitialize中的count與申請時的capacity需要一致
  • 使用完后必須deallocate

1.2 raw pointer

raw pointer也叫做原生指針,就是指未指定數(shù)據(jù)類型的指針。

使用原生指針需要注意2點??

  1. 對于指針的內(nèi)存管理是需要手動管理
  2. 指針在使用完需要手動釋放

例如??

//定義一個未知類型的指針:本質(zhì)是分配32字節(jié)大小的空間,指定對齊方式是8字節(jié)對齊
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

//存值
for i in 0..<4 {
    p.storeBytes(of: i + 1, as: Int.self)
}
//取值
for i in 0..<4 {
    //p是當(dāng)前內(nèi)存的首地址,通過內(nèi)存平移來獲取值
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index: \(i), value: \(value)")
}

//使用完需要手動釋放
p.deallocate()

運行??

上圖可見,讀取出來的值不對,原因是存值for循環(huán)時,p中每8個字節(jié)存儲的是i+1,但是卻沒有指定i+1這個值占的內(nèi)存大小,也就是沒有指定值的內(nèi)存大小

解決:通過advanced(by:)指定存儲時的步長。

for i in 0..<4 {
    //指定當(dāng)前移動的步數(shù),即i * 8
    p.advanced(by: i * 8).storeBytes(of: i + 1, as: Int.self)
}

二、相關(guān)應(yīng)用場景

2.1 訪問結(jié)構(gòu)體對象

我們先定義一個結(jié)構(gòu)體??

struct LGTeacher {
    var age = 18
    var height = 1.85
}
var t = LGTeacher()

然后我們使用UnsafeMutablePointer創(chuàng)建指針,然后訪問結(jié)構(gòu)體對象t,代碼??

// 分配2個LGTeacher大小的空間
let ptr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 2)
// 初始化第一個空間
ptr.initialize(to: LGTeacher())
// 移動,初始化第2個空間
ptr.successor().initialize(to: LGTeacher(age: 20, height: 1.75))

//訪問方式一 下標(biāo)訪問
print(ptr[0])
print(ptr[1])

//訪問方式二 內(nèi)存平移
print(ptr.pointee)
print((ptr+1).pointee)

//訪問方式三 successor()
print(ptr.pointee)
//successor 往前移動
print(ptr.successor().pointee)

//必須和分配是一致的
ptr.deinitialize(count: 2)
//釋放
ptr.deallocate()

有3種方式訪問 ?? 下標(biāo) + 內(nèi)存平移 + successor()。運行??

那能否通過advanced(by: MemoryLayout)的方式來訪問呢?改一下初始化第二個LGPerson的代碼??

ptr.advanced(by: MemoryLayout<LGTeacher>.stride).initialize(to: LGTeacher(age: 20, height: 1.80))

運行看看??

第二個LGPerson的數(shù)據(jù)出問題了,看來通過advanced(by: MemoryLayout)指定步長的方式是錯誤的。正確的方式可以有以下幾種??

// 內(nèi)存平移
(ptr + 1).initialize(to: CJLTeacher(age: 20, height: 1.80))

// successor()
ptr.successor().initialize(to: CJLTeacher(age: 20, height: 1.80))

// advanced(by: 1)
ptr.advanced(by: 1).initialize(to:  CJLTeacher(age: 20, height: 1.80))

那為什么advanced(by: MemoryLayout<LGTeacher>.stride)不行,但是advanced(by: 1)卻可以呢?

  1. advanced(by: MemoryLayout<LGTeacher>.stride) 的移動步長是類LGTeacher實例的大小 -->32 = 16(metadata8 + refcount8) + 16(age8+height8),而advanced(by: 1)是移動步長為1。
  2. 關(guān)鍵在于這句代碼-->let ptr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 2),此時我們是知道ptr的具體類型的,就是指向LGTeacher的指針

所以在確定指針的類型后,通過步長的移動+1,就表示移動了那個類的實例大小空間+1。

2.2 實例對象綁定到struct的內(nèi)存

示例代碼??

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

class LGTeacher{
    var age = 18
}

var t = LGTeacher()

我們將 LGTeacher實例對象t綁定到結(jié)構(gòu)體HeapObject中,代碼??

//將t綁定到結(jié)構(gòu)體內(nèi)存中
//1、獲取實例變量的內(nèi)存地址,聲明成了非托管對象
/*
 通過Unmanaged指定內(nèi)存管理,類似于OC與CF的交互方式(所有權(quán)的轉(zhuǎn)換 __bridge)
 - passUnretained 不增加引用計數(shù),即不需要獲取所有權(quán)
 - passRetained 增加引用計數(shù),即需要獲取所有權(quán)
 - toOpaque 不透明的指針
 */

let ptr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
//2、綁定到結(jié)構(gòu)體內(nèi)存,返回值是UnsafeMutablePointer<T>
/*
 - bindMemory 更改當(dāng)前 UnsafeMutableRawPointer 的指針類型,綁定到具體的類型值
    - 如果沒有綁定,則綁定
    - 如果已經(jīng)綁定,則重定向到 HeapObject類型上
 */
let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
//3、訪問成員變量
print(heapObject.pointee.kind)
print(heapObject.pointee.strongRef)
print(heapObject.pointee.unownedRef)

運行??

再將kind的類型改成UnsafeRawPointer??

struct HeapObject {
    var kind: UnsafeRawPointer
    var strongRef: UInt32
    var unownedRef: UInt32
}

運行??

kind的輸出就是地址了。

接著我們換一個結(jié)構(gòu)體 --> Swift類Class對應(yīng)的底層的結(jié)構(gòu)體??

struct lg_swift_class {
    var kind: UnsafeRawPointer
    var superClass: UnsafeRawPointer
    var cachedata1: UnsafeRawPointer
    var cachedata2: UnsafeRawPointer
    var data: UnsafeRawPointer
    var flags: UInt32
    var instanceAddressOffset: UInt32
    var instanceSize: UInt32
    var flinstanceAlignMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressOffset: UInt32
    var description: UnsafeRawPointer
}

因為kind也是一個指針,我們再將kind綁定到lg_swift_class結(jié)構(gòu)體,代碼??

let metaPtr = heapObject.pointee.kind.bindMemory(to: lg_swift_class.self, capacity: 1)
print(metaPtr.pointee)

運行??

2.3 元組指針類型轉(zhuǎn)換

示例??

var tul = (10, 20)

//UnsafePointer<T>
func testPointer(_ p : UnsafePointer<Int>){
    print(p)
}

withUnsafePointer(to: &tul) { (tulPtr: UnsafePointer<(Int, Int)>) in
    //不能使用bindMemory,因為已經(jīng)綁定到具體的內(nèi)存中了
    //使用assumingMemoryBound,假定內(nèi)存綁定,目的是告訴編譯器ptr已經(jīng)綁定過Int類型了,不需要再檢查memory綁定
    testPointer(UnsafeRawPointer(tulPtr).assumingMemoryBound(to: Int.self))
}

上面示例是將元組tul的指針類型 UnsafePointer<(Int, Int)>)轉(zhuǎn)換成了UnsafePointer<Int>

也可以直接告訴編譯器轉(zhuǎn)換成具體的類型??

func testPointer(_ p: UnsafeRawPointer){
    p.assumingMemoryBound(to: Int.self)
}

2.4 獲取結(jié)構(gòu)體的成員變量的指針

示例??

struct HeapObject {
    var strongRef: UInt32 = 10
    var unownedRef: UInt32 = 20
}

func testPointer(_ p: UnsafePointer<Int>){
   print(p)
}
//實例化
var  t = HeapObject()
//獲取結(jié)構(gòu)體屬性的指針傳入函數(shù)
withUnsafePointer(to: &t) { (ptr: UnsafePointer<HeapObject>) in
    //獲取變量
    let strongRef = UnsafeRawPointer(ptr) + MemoryLayout<HeapObject>.offset(of: \HeapObject.strongRef)!
    //傳遞strongRef屬性的值
    testPointer(strongRef.assumingMemoryBound(to: Int.self))
}

通過withUnsafePointer將t綁定到結(jié)構(gòu)體HeapObject內(nèi)存中,然后通過 MemoryLayout<HeapObject>.offset()內(nèi)存平移獲取結(jié)構(gòu)體成員變量strongRef,最后通過assumingMemoryBound進行內(nèi)存的綁定。

注意:assumingMemoryBound假定內(nèi)存綁定,這里就是告訴編譯器:我的類型就是這個,你不要檢查我了,其實際類型還是原來的類型。

運行??

2.5 臨時綁定內(nèi)存類型

先看以下示例代碼??

var age = 10

func testPointer(_ p: UnsafePointer<Int64>){
   print(p)
}

withUnsafePointer(to: &age) { (ptr: UnsafePointer<Int>) in
    testPointer(ptr)
}

會報錯:指針類型不一致!

解決方案:通過withMemoryRebound臨時綁定內(nèi)存類型 ??

var age = 10
func testPointer(_ p: UnsafePointer<Int64>){
   print(p)
}
let ptr = withUnsafePointer(to: &age) {$0}
ptr.withMemoryRebound(to: Int64.self, capacity: 1) { (ptr: UnsafePointer<Int64>)  in
    testPointer(ptr)
}

總結(jié)

本篇文章主要講解了Swift中的2大指針類型:typed pointerraw pointer,然后講解了指針的幾個常見的應(yīng)用場景,包含更改內(nèi)存綁定的類型,假定內(nèi)存綁定臨時更改內(nèi)存綁定類型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容