Swift 指針
前言
指針,作為編程中最重要的概念,一直存在于各大語言中,下面我們就來探索一下Swift
中的指針。
1. Swift 指針簡介
Swift
中的指針分為兩類:
-
raw pointer
:未指定數據類型的指針(原生指針),在Swift
中的表示是UnsafeRawPointer
,我們只知道這是個指針,并不知道其內部存儲的類型 -
typed pointer
:指定數據類型的指針,在Swift
中的表示是UnsafePointer<T>
,其中T
表示泛型,指針內部存儲的是T
類型的數據
swift與OC指針的對比:
Swift | Objective-C | 說明 |
---|---|---|
unsafePointer<T> | const T * | 指針及所指向的內容都不可變 |
UnsafeMutablePointer<T> | T * | 指針及其所指向的內存內容均可變 |
UnsafeRawPointer | const void * | 指針指向不可變未知類型 |
UnsafeMutableRawPointer | void * | 指針指向可變未知類型 |
2. raw pointer簡單使用
2.1 示例
注: 對于raw pointer
首先要說明一下,其內存管理是手動管理的,指針在使用完畢后需要手動釋放
舉個例子:
// 定義一個未知類型的的指針p,分配32字節大小的空間,8字節對齊
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 {
// 從首地址開始偏移讀取
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index: \(i) value: \(value)")
}
// 釋放內存
p.deallocate()
運行結果:
從運行結果中可以看到,這并不是我們想要的結果,這也是我們平常在使用的時候需要特別注意的。因為我們在讀取的時候是從指針首地址進行不斷的偏移讀取的,但是存儲的時候卻都是存儲在了首地址,所以存儲的時候也要進行偏移。修改后的代碼如下:
// 定義一個未知類型的的指針p,分配32字節大小的空間,8字節對齊
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
// 存儲數據
for i in 0..<4 {
// p.storeBytes(of: i + 1, as: Int.self)
// 修改后(每次存儲的位置都有增加,也就是偏移)
p.advanced(by: i * 8).storeBytes(of: i + 1, as: Int.self)
}
// 讀取數據
for i in 0..<4 {
// 從首地址開始偏移讀取
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index: \(i) value: \(value)")
}
// 釋放內存
p.deallocate()
打印結果:
2.2 allocate 源碼
我們來看看allocate
函數的源碼:(在UnsafeRawPointer.Swift文件中)
/// Allocates uninitialized memory with the specified size and alignment.
///
/// You are in charge of managing the allocated memory. Be sure to deallocate
/// any memory that you manually allocate.
///
/// The allocated memory is not bound to any specific type and must be bound
/// before performing any typed operations. If you are using the memory for
/// a specific type, allocate memory using the
/// `UnsafeMutablePointer.allocate(capacity:)` static method instead.
///
/// - Parameters:
/// - byteCount: The number of bytes to allocate. `byteCount` must not be negative.
/// - alignment: The alignment of the new region of allocated memory, in
/// bytes.
/// - Returns: A pointer to a newly allocated region of memory. The memory is
/// allocated, but not initialized.
@inlinable
public static func allocate(
byteCount: Int, alignment: Int
) -> UnsafeMutableRawPointer {
// For any alignment <= _minAllocationAlignment, force alignment = 0.
// This forces the runtime's "aligned" allocation path so that
// deallocation does not require the original alignment.
//
// The runtime guarantees:
//
// align == 0 || align > _minAllocationAlignment:
// Runtime uses "aligned allocation".
//
// 0 < align <= _minAllocationAlignment:
// Runtime may use either malloc or "aligned allocation".
var alignment = alignment
if alignment <= _minAllocationAlignment() {
alignment = 0
}
return UnsafeMutableRawPointer(Builtin.allocRaw(
byteCount._builtinWordValue, alignment._builtinWordValue))
}
該方法就是:
- 以指定的大小和對齊方式分配未初始化的內存
- 首先對對齊方式進行校驗
- 然后調用
Builtin.allocRaw
方法進行分配內存 -
Builtin
是Swift
的標準模塊,可以理解為調用(匹配)LLVM
中的方法
3. typed pointer
在前幾篇文章中打印內存地址的時候就用過withUnsafePointer
,下面我們就來看看。
3.1 定義
/// Invokes the given closure with a pointer to the given argument.
///
/// The `withUnsafePointer(to:_:)` function is useful for calling Objective-C
/// APIs that take in parameters by const pointer.
///
/// The pointer argument to `body` is valid only during the execution of
/// `withUnsafePointer(to:_:)`. Do not store or return the pointer for later
/// use.
///
/// - Parameters:
/// - value: An instance to temporarily use via pointer.
/// - body: A closure that takes a pointer to `value` as its sole argument. If
/// the closure has a return value, that value is also used as the return
/// value of the `withUnsafePointer(to:_:)` function. The pointer argument
/// is valid only for the duration of the function's execution.
/// It is undefined behavior to try to mutate through the pointer argument
/// by converting it to `UnsafeMutablePointer` or any other mutable pointer
/// type. If you need to mutate the argument through the pointer, use
/// `withUnsafeMutablePointer(to:_:)` instead.
/// - Returns: The return value, if any, of the `body` closure.
@inlinable public func withUnsafePointer<T, Result>(to value: T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
該函數一共兩個參數:
- 第一個就是要獲取其指針的變量
- 第二個是一個閉包,然后通過
rethrows
關鍵字重新拋出Result
(也就是閉包表達式的返回值),閉包的參數和返回值都是泛型,關于這種寫法可以縮寫,詳見后面的代碼。
3.2 簡單使用
3.2.1 常見用法
示例代碼:
var a = 10
/**
通過Swift提供的簡寫的API,這里是尾隨閉包的寫法
返回值的類型是 UnsafePointer<Int>
*/
let p = withUnsafePointer(to: &a) { $0 }
print(p)
withUnsafePointer(to: &a) {
print($0)
}
// Declaration let p1:UnsafePointer<Int>
let p1 = withUnsafePointer(to: &a) { ptr in
return ptr
}
print(p1)
打印結果:
以上三種用法是我們最常用的三種方法,都能夠打印出變量的指針。那么是否可以通過指針修改變量的值呢?下面我們就來研究一下:
3.2.2 通過指針獲取變量值
要想改變值,首先就要能夠訪問到變量的值:
let p = withUnsafePointer(to: &a) { $0 }
print(p.pointee)
withUnsafePointer(to: &a) {
print($0.pointee)
}
let p1 = withUnsafePointer(to: &a) { ptr in
return ptr
}
print(p1.pointee)
let p2 = withUnsafePointer(to: &a) { ptr in
return ptr.pointee
}
print(p2)
打印結果:
3.2.2 通過指針修改變量值:
如果使用的是withUnsafePointer
是不能直接在閉包中修改指針的:
但是我們可以通過間接的方式,通過返回值修改,給原來變量賦值的方式修改(其實這種方式很low)
a = withUnsafePointer(to: &a){ ptr in
return ptr.pointee + 2
}
print(a)
打印結果:
我們可以使用withUnsafeMutablePointer
,直接修改變量的值。
withUnsafeMutablePointer(to: &a){ ptr in
ptr.pointee += 2
}
打印結果:
還有另一種方式,就是通過創建指針的方式,這也是一種創建Type Pointer
的方式:
// 創建一個指針,指針內存存儲的是Int類型數據,開辟一個8*1字節大小的區域
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//初始化指針
ptr.initialize(to: a)
// 修改
ptr.pointee += 2
print(a)
print(ptr.pointee)
// 反初始化,與下面的代碼成對調用,管理內存
ptr.deinitialize(count: 1)
// 釋放內存
ptr.deallocate()
從這里我們可以看到,指針的值在修改后是變了的,但是原變量的值并沒有改變。所以不能用于直接修改原變量。
4. 實戰案例
4.1 案例一
本案例是初始化一個指針,能夠訪問兩個結構體實例對象。
首先定義一個結構體
struct Teacher {
var age = 18
var height = 1.65
}
下面我們通過三種方式訪問指針中的結構體對象
- 通過下標訪問
- 通過內存平移訪問
- 通過successor()函數訪問
// 分配兩個Teacher大小空間的指針
let ptr = UnsafeMutablePointer<Teacher>.allocate(capacity: 2)
// 初始化第一個Teacher
ptr.initialize(to: Teacher())
// 初始化第二個Teacher
ptr.successor().initialize(to: Teacher(age: 20, height: 1.85))
// 錯誤的初始化方式,因為這是確定類型的指針,只需移動一步即移動整個類型大小的內存
//ptr.advanced(by: MemoryLayout<Teacher>.stride).initialize(to: Teacher(age: 20, height: 1.85))
// 通過下標訪問
print(ptr[0])
print(ptr[1])
// 內存偏移
print(ptr.pointee)
print((ptr+1).pointee)
// successor
print(ptr.pointee)
print(ptr.successor().pointee)
// 反初始化,釋放內存
ptr.deinitialize(count: 2)
ptr.deallocate()
打印結果:
這里有一點需要特別注意:
我們在初始化的時候,如果需要在內存后面繼續初始化,平移的時候,對于已知類型的指針,每平移一步就是已知類型所占內存大小。如果按照上面內存的寫法就是16*16個字節的大小,移動了16個已知類型數據的內存大小,這樣就跟我們實際的需求不符了。
4.2 案例二
我們知道在Swift
中對象的本質是HeapObject
,下面我們就通過內存的方式,將實例對象綁定到我們自定義的HeapObject
上。
首先定義如下代碼:
struct HeapObject {
var kind: UnsafeRawPointer
var strongRef: UInt32
var unownedRef: UInt32
}
class Teacher {
var age: Int = 18
}
var t = Teacher()
指針綁定:
/**
使用withUnsafeMutablePointer獲取到的指針是UnsafeMutablePointer<T>類型
UnsafeMutablePointer<T>沒有bindMemory方法
所以此處引入Unmanaged
*/
//let ptr = withUnsafeMutablePointer(to: &t) { $0 }
/**
Unmanaged 指定內存管理,類似于OC與CF交互時的所有權轉換__bridge
Unmanaged 有兩個函數:
- passUnretained:不增加引用計數,也就是不獲得所有權
- passRetained: 增加引用計數,也就是可以獲得所有權
以上兩個函數,可以通過toOpaque函數返回一個
UnsafeMutableRawPointer 指針
*/
let ptr = Unmanaged.passUnretained(t).toOpaque()
//let ptr = Unmanaged.passRetained(t).toOpaque()
/**
bindMemory :將指針綁定到指定類型數據上
如果沒有綁定則綁定
已綁定則重定向到指定類型上
*/
let h = ptr.bindMemory(to: HeapObject.self, capacity: 1)
print(h.pointee)
運行結果:
4.3 案例三
既然可以綁定對象,那么我們也就可以綁定類,接著案例二中的一些代碼,kind
對應的就是metaData
。
首先我們將swift
中的類結構定義成一個內存結構一致的結構體:
struct swiftClass {
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,綁定成上面的結構體。
4.4 案例四
在實際開發中,我們往往會調用其他人的api完成一些代碼,在指針操作過程中,往往會因為類型不匹配造成傳參問題,下面我們就來看一個例子:
首先我們定義一個打印指針的函數:
func printPointer(p: UnsafePointer<Int>) {
print(p)
print("end")
}
此時我們在定義一個元組,我們要打印元組指針,或者通過指針獲取一些數據,按照以前的寫法就會報如下編譯錯誤:
那么該如何解決該問題呢?首先不能修改函數,因為這個函數是通用的,也不一定都是我們自己定義的。此時我們通過強轉和assumingMemoryBound
來解決這個問題,示例代碼:
withUnsafeMutablePointer(to: &tul) { tulPtr in
printPointer(p: UnsafeRawPointer(tulPtr).assumingMemoryBound(to: Int.self))
}
lldb打印結果:
assumingMemoryBound
是假定內存綁定,目的是告訴編譯器已經綁定過Int
類型了,不需要在檢查內存綁定。
那么我們將元組換成結構體呢?我們此時想要獲取結構體中的屬性。
struct Test {
var a: Int = 10
var b: Int = 20
}
var t = Test()
withUnsafeMutablePointer(to: &t) { ptr in
printPointer(p: UnsafeRawPointer(ptr).assumingMemoryBound(to: Int.self))
}
其實是沒有任何區別的
打印結果:
那么如果我想想獲取結構體中的屬性呢?
withUnsafeMutablePointer(to: &t) { ptr in
// let strongRefPtr = withUnsafePointer(to: &ptr.pointee.b) { $0 }
// printPointer(p: strongRefPtr)
// let strongRefPtr = UnsafeRawPointer(ptr).advanced(by: MemoryLayout<Int>.stride)
let strongRefPtr = UnsafeRawPointer(ptr) - MemoryLayout<Test>.offset(of: \Test.b)!
printPointer(p: strongRefPtr.assumingMemoryBound(to: Int.self))
}
打印結果:
以上提供了三種方式實現訪問結構體中的屬性。
4.5 案例五
使用withMemoryRebound
臨時更改內存綁定類型,withMemoryRebound
的主要應用場景還是處理一些類型不匹配的場景,將內存綁定類型臨時修改成想要的類型,在閉包里生效,不會修改原指針的內存綁定類型。
var age: UInt64 = 18
let ptr = withUnsafePointer(to: &age) { $0 }
// 臨時更改內存綁定類型
ptr.withMemoryRebound(to: Int.self, capacity: 1) { (ptr) in
printPointer(p: ptr)
}
func printPointer(p: UnsafePointer<Int>) {
print(p)
print("end")
}
5. 總結
至此我們對Swift
中指針的分析基本就完事了,現在總結如下:
-
Swift
中的指針分為兩種:-
raw pointer
:未指定類型(原生)指針,即:UnsafeRawPointer
和unsafeMutableRawPointer
-
type pointer
:指定類型的指針,即:UnsafePointer<T>
和UnsafeMutablePointer<T>
-
- 指針的綁定主要有三種方式:
-
withMemoryRebound
:臨時更改內存綁定類型 -
bingMemory(to: capacity:)
:更改內存綁定的類型,如果之前沒有綁定,那么就是首次綁定,如果綁定過了,會被重新綁定為該類型 -
assumingMemoryBound
:假定內存綁定,這里就是告訴編譯器,我就是這種類型,不要檢查了(控制權由我們決定)
-
- 內存指針的操作都是危險的,使用時要慎重