前言
本篇文章主要講解一下Swift中的指針
,以及相關(guān)的應(yīng)用場景
,指針
也是面試官經(jīng)常問到的知識點,希望大家能夠掌握。
一、指針類別
Swift中的指針分為兩類
-
typed pointer
: 指定數(shù)據(jù)類型指針,即UnsafePointer<T>
,其中T表示泛型
-
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種方式??
- 通過
withUnsafeMutablePointer
方法??
var age = 18
withUnsafeMutablePointer(to: &age) { ptr in
ptr.pointee += 12
}
- 通過
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
,需要注意以下幾點??
initialize
與deinitialize
需成對使用deinitialize
中的count
與申請時的capacity
需要一致
- 使用完后必須
deallocate
1.2 raw pointer
raw pointer也叫做原生指針
,就是指未指定數(shù)據(jù)類型
的指針。
使用
原生指針
需要注意2點??
- 對于指針的內(nèi)存管理是需要
手動管理
的- 指針在使用完需要
手動釋放
例如??
//定義一個未知類型的指針:本質(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)
卻可以呢?
-
advanced(by: MemoryLayout<LGTeacher>.stride)
的移動步長是類LGTeacher實例的大小 -->32 =16(metadata8 + refcount8) + 16(age8+height8)
,而advanced(by: 1)
是移動步長為1
。 - 關(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 pointer
和raw pointer
,然后講解了指針的幾個常見的應(yīng)用場景,包含更改內(nèi)存綁定的類型
,假定內(nèi)存綁定
和臨時更改內(nèi)存綁定類型
。