Swift 指針

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()

運行結果:

image

從運行結果中可以看到,這并不是我們想要的結果,這也是我們平常在使用的時候需要特別注意的。因為我們在讀取的時候是從指針首地址進行不斷的偏移讀取的,但是存儲的時候卻都是存儲在了首地址,所以存儲的時候也要進行偏移。修改后的代碼如下:

// 定義一個未知類型的的指針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()

打印結果:

image

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方法進行分配內存
  • BuiltinSwift的標準模塊,可以理解為調用(匹配)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)

打印結果:

image

以上三種用法是我們最常用的三種方法,都能夠打印出變量的指針。那么是否可以通過指針修改變量的值呢?下面我們就來研究一下:

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)

打印結果:

image

3.2.2 通過指針修改變量值:

如果使用的是withUnsafePointer是不能直接在閉包中修改指針的:

image

但是我們可以通過間接的方式,通過返回值修改,給原來變量賦值的方式修改(其實這種方式很low)

a = withUnsafePointer(to: &a){ ptr in
    return ptr.pointee + 2
}

print(a)

打印結果:

image

我們可以使用withUnsafeMutablePointer,直接修改變量的值。

withUnsafeMutablePointer(to: &a){ ptr in
    ptr.pointee += 2
}

打印結果:

image

還有另一種方式,就是通過創建指針的方式,這也是一種創建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()
image

從這里我們可以看到,指針的值在修改后是變了的,但是原變量的值并沒有改變。所以不能用于直接修改原變量。

4. 實戰案例

4.1 案例一

本案例是初始化一個指針,能夠訪問兩個結構體實例對象。

首先定義一個結構體

struct Teacher {
    var age = 18
    var height = 1.65
}

下面我們通過三種方式訪問指針中的結構體對象

  1. 通過下標訪問
  2. 通過內存平移訪問
  3. 通過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()

打印結果:

image

這里有一點需要特別注意:

我們在初始化的時候,如果需要在內存后面繼續初始化,平移的時候,對于已知類型的指針,每平移一步就是已知類型所占內存大小。如果按照上面內存的寫法就是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)

運行結果:

image

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,綁定成上面的結構體。

image

4.4 案例四

在實際開發中,我們往往會調用其他人的api完成一些代碼,在指針操作過程中,往往會因為類型不匹配造成傳參問題,下面我們就來看一個例子:

首先我們定義一個打印指針的函數:

func printPointer(p: UnsafePointer<Int>) {
    print(p)
    print("end")
}

此時我們在定義一個元組,我們要打印元組指針,或者通過指針獲取一些數據,按照以前的寫法就會報如下編譯錯誤:

image

那么該如何解決該問題呢?首先不能修改函數,因為這個函數是通用的,也不一定都是我們自己定義的。此時我們通過強轉和assumingMemoryBound來解決這個問題,示例代碼:

withUnsafeMutablePointer(to: &tul) { tulPtr in
    printPointer(p: UnsafeRawPointer(tulPtr).assumingMemoryBound(to: Int.self))
}

lldb打印結果:


image

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))
}

其實是沒有任何區別的

打印結果:

image

那么如果我想想獲取結構體中的屬性呢?

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))
}

打印結果:

image

以上提供了三種方式實現訪問結構體中的屬性。

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中指針的分析基本就完事了,現在總結如下:

  1. Swift中的指針分為兩種:
    1. raw pointer:未指定類型(原生)指針,即:UnsafeRawPointerunsafeMutableRawPointer
    2. type pointer:指定類型的指針,即:UnsafePointer<T>UnsafeMutablePointer<T>
  2. 指針的綁定主要有三種方式:
    1. withMemoryRebound:臨時更改內存綁定類型
    2. bingMemory(to: capacity:):更改內存綁定的類型,如果之前沒有綁定,那么就是首次綁定,如果綁定過了,會被重新綁定為該類型
    3. assumingMemoryBound:假定內存綁定,這里就是告訴編譯器,我就是這種類型,不要檢查了(控制權由我們決定)
  3. 內存指針的操作都是危險的,使用時要慎重
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • 為了避免疏漏, 我從官方文檔作了截圖, 蘋果官網文檔1 , 文檔2 本文概要 按照官方文檔, 介紹Swift中的指...
    Lin__Chuan閱讀 2,700評論 12 9
  • 前言 本篇文章主要講解一下Swift中的指針,以及相關的應用場景,指針也是面試官經常問到的知識點,希望大家能夠掌握...
    深圳_你要的昵稱閱讀 3,461評論 0 11
  • 本文系學習Swift中的指針操作詳解的整理 默認情況下Swift是內存安全的,蘋果官方不鼓勵我們直接操作內存。但是...
    流火緋瞳閱讀 15,103評論 2 28
  • 參考文獻 Swift結構體指針操作官方文檔Swift 和 C 不得不說的故事Swift指針和托管,你看我就夠了 W...
    沉靜BBQ閱讀 8,050評論 1 25
  • 指針 Swift中的指針分為兩類, typed pointer指定數據類型指針,raw pinter未指定數據類型...
    Mjs閱讀 566評論 0 1