[Swift] 指針UnsafePointer

本文系學習Swift中的指針操作詳解的整理

默認情況下Swift是內存安全的,蘋果官方不鼓勵我們直接操作內存。但是,Swift中也提供了使用指針操作內存的方法,直接操作內存是很危險的行為,很容易就出現錯誤,因此官方將直接操作內存稱為 “unsafe 特性”。

在操作指針之前,需要理解幾個概念:size、alignment、stride,以及他們的獲取/使用,這就用到了 MemoryLayout :

MemoryLayout

使用MemoryLayout,可以檢測某個類型的實際大小(size),內存對齊大小(alignment),以及實際占用的內存大小(步長:stride),其單位均為字節;

public enum MemoryLayout<T> {

    public static var size: Int { get }

    public static var stride: Int { get }

    public static var alignment: Int { get }

    public static func size(ofValue value: T) -> Int

    public static func stride(ofValue value: T) -> Int

    public static func alignment(ofValue value: T) -> Int
}

例如:如果一個類型的大小(size)為5字節,對齊內存(alignment)大小為4字節,那么其實際占用的內存大小(stride)為8字節,這是因為編譯需要為其填充空白的邊界,使其符合它的 4 字節內存邊界對齊。

常見基本類型的內存size、alignment、stride:

MemoryLayout<Int>.size // return 8 (on 64-bit)
MemoryLayout<Int>.alignment // return 8 (on 64-bit)
MemoryLayout<Int>.stride // return 8 (on 64-bit)

MemoryLayout<Int16>.size // return 2
MemoryLayout<Int16>.alignment // return 2
MemoryLayout<Int16>.stride // return 2

MemoryLayout<Bool>.size // return 1
MemoryLayout<Bool>.alignment // return 1
MemoryLayout<Bool>.stride // return 1

MemoryLayout<Float>.size // return 4
MemoryLayout<Float>.alignment // return 4
MemoryLayout<Float>.stride // return 4

MemoryLayout<Double>.size // return 8
MemoryLayout<Double>.alignment // return 8
MemoryLayout<Double>.stride // return 8

原文中Bool的相關值為2,在Playground中測試結果為1,所以這里更改為了1

一般在移動指針的時候,對于特定類型,指針一次移動一個stride(步長),移動的范圍,要在分配的內存范圍內,切記超出分配的內存空間,切記超出分配的內存空間,切記超出分配的內存空間。
一般情況下stride是alignment的整數倍,即符合內存對齊原則;實際分配的內存空間大小也是alignment的整數倍,但是實際實例大小可能會小于實際分配的內存空間大小。

UnsafePointer

所有指針類型為 UnsafePointer,一旦你操作了內存,編譯器不會對這種操作進行檢測,你需要對自己的代碼承擔全部的責任。Swift中定義了一些特定類型的指針,每個類型都有他們的作用和目的,使用適當的指針類型可以防止錯誤的發生,并且更清晰得表達開發者的意圖,防止未定義行為的產生。

通過指針類型的名稱,我們可以知道這是一個什么類型的指針:可變/不可變、原生(raw)/有類型、是否是緩沖類型(buffer),大致有以下8種類型:

Pointer Name Unsafe? Write Access? Collection Strideable? Typed?
UnsafeMutablePointer<T> yes yes no yes yes
UnsafePointer<T> yes no no yes yes
UnsafeMutableBufferPointer<T> yes yes yes no yes
UnsafeBufferPointer<T> yes no yes no yes
UnsafeRawPointer yes no no yes no
UnsafeMutableRawPointer yes yes no yes no
UnsafeMutableRawBufferPointer yes yes yes no no
UnsafeRawBufferPointer yes no yes no no
  • unsafe:不安全的
  • Write Access:可寫入
  • Collection:像一個容器,可添加數據
  • Strideable:指針可使用 advanced 函數移動
  • Typed:是否需要指定類型(范型)

原生(Raw)指針

// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let aligment = MemoryLayout<Int>.alignment
let byteCount = stride * count

// 2
do {
    print("raw pointers")
    // 3
    let pointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: aligment)
    
    // 4
    defer {
        pointer.deallocate()
        
    }
    
    // 5
    pointer.storeBytes(of: 42, as: Int.self)
    pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
// 讀取第一個值
    pointer.load(as: Int.self)
// 讀取第二個值
    pointer.advanced(by: stride).load(as: Int.self)
    
    // 6
    let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
    for (index, byte) in bufferPointer.enumerated() {
        print("bute \(index): \(byte)")
    }
}

代碼說明:

    1. 聲明示例用到的參數
      count :整數的個數
      stride:整數的步長
      aligment:整數的內存對齊大小
      byteCount:實際需要的內存大小
    1. 聲明作用域
      使用 do 來增加一個作用域,讓我們可以在接下的示例中復用作用域中的變量名。
    1. UnsafeMutableRawPointer.allocate 創建分配所需字節數,該指針可以用來讀取和存儲(改變)原生的字節。

這里分配內存使用了allocate 方法:

public static func allocate(byteCount: Int, alignment: Int) -> UnsafeMutableRawPointer
  • byteCount:所需字節數
  • alignment:內存對齊
    1. 延時釋放
      使用 defer 來保證內存得到正確地釋放,操作指針的時候,所有內存都需要我們手動進行管理。
      這里釋放內存使用了deallocate方法:
 public func deallocate()

allocate 和 deallocate 方法一定要配對出現。

    1. 使用 storeBytes 和 load 方法存儲和讀取字節

存儲數據方法storeBytes:

/// - Parameters:
///   - value: The value to store as raw bytes.
///   - offset: The offset from this pointer, in bytes. `offset` must be
///     nonnegative. The default is zero.
///   - type: The type of `value`.
public func storeBytes<T>(of value: T, toByteOffset offset: Int = default, as type: T.Type)
  • value:要存儲的值
  • offset:偏移量,默認即可
  • type:值的類型

讀取數據方法 load :

/// - Parameters:
    ///   - offset: The offset from this pointer, in bytes. `offset` must be
    ///     nonnegative. The default is zero.
    ///   - type: The type of the instance to create.
    /// - Returns: A new instance of type `T`, read from the raw bytes at
    ///   `offset`. The returned instance is memory-managed and unassociated
    ///   with the value in the memory referenced by this pointer.
    public func load<T>(fromByteOffset offset: Int = default, as type: T.Type) -> T
  • offset:偏移量,默認即可
  • type:值的類型

移動指針地址 advanced :

/// - Parameter n: The number of bytes to offset this pointer. `n` may be
    ///   positive, negative, or zero.
    /// - Returns: A pointer offset from this pointer by `n` bytes.
    public func advanced(by n: Int) -> UnsafeMutableRawPointer
  • n:步長stride

使用原生指針,存儲下一個值的時候需要移動一個步長(stride),也可以直接使用 + 運算符:

(pointer + stride).storeBytes(of: 6, as: Int.self)
    1. UnsafeRawBufferPointer 類型以字節流的形式來讀取內存。這意味著我們可以這些字節進行迭代,對其使用下標,或者使用 filter,map 以及 reduce 這些很酷的方法,緩沖類型指針使用了原生指針進行初始化。

類型指針

do {
    print("Typed pointers")
    // 1.
    let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
    pointer.initialize(repeating: 0, count: count)

    // 2.
    defer {
        pointer.deinitialize(count: count)
        pointer.deallocate()
    }
    // 3.
    pointer.pointee = 42
    pointer.advanced(by: 1).pointee = 6
    pointer.pointee
    pointer.advanced(by: 1).pointee
    
    let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
    for (index, value) in bufferPointer.enumerated() {
        print("value \(index): \(value)")
    }
    
}

類型指針與原生指針的區別,主要體現在上面標注數字的幾個地方:

    1. 分配內存、初始化

類型指針,在分配內存的時候通過給范型賦值來指定當前指針所操作的數據類型:

/// - Parameter count: The amount of memory to allocate, counted in instances
    ///   of `Pointee`.
    public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
  • count:要存儲的數據個數

可以看到其分配內存的方法,只有一個參數,指定所要存儲的數據個數即可,因為通過給范型參數賦值,已經知道了要存儲的數據類型,其alignment和stride就確定了,這時只需要再知道存儲幾個數據即可。

這里還多了個初始化的過程,類型指針單單分配內存,還不能使用,還需要初始化:

/// - Parameters:
    ///   - repeatedValue: The instance to initialize this pointer's memory with.
    ///   - count: The number of consecutive copies of `newValue` to initialize.
    ///     `count` must not be negative. 
    public func initialize(repeating repeatedValue: Pointee, count: Int)
  • repeatedValue:默認值
  • count:數量
    1. 延時釋放

在釋放的時候,要先釋放已初始化的實例(deinitialize),再釋放已分配的內存(deallocate)空間:

/// - Parameter count: The number of instances to deinitialize. `count` must
    ///   not be negative. 
    /// - Returns: A raw pointer to the same address as this pointer. The memory
    ///   referenced by the returned raw pointer is still bound to `Pointee`.
    public func deinitialize(count: Int) -> UnsafeMutableRawPointer
  • count:數量
    1. 存儲/讀取
      類型指針的存儲/讀取值,不需要再使用storeBytes/load,Swift提供了一個以類型安全的方式讀取和存儲值--pointee:
public var pointee: Pointee { get nonmutating set }

這里的移動指針的方法,和上面的一致,也是 advanced ,但是其參數有所不同:

/// - Parameter n: The number of strides of the pointer's `Pointee` type to
    ///   offset this pointer. To access the stride, use
    ///   `MemoryLayout<Pointee>.stride`. `n` may be positive, negative, or
    ///   zero.
    /// - Returns: A pointer offset from this pointer by `n` instances of the
    ///   `Pointee` type.
    public func advanced(by n: Int) -> UnsafeMutablePointer<Pointee>
  • n:這里是按類型值的個數進行移動

同樣,這里也可以使用運算符 + 進行移動:

(pointer + 1).pointee = 6

原生指針轉換為類型指針

do {
    print("Converting raw pointers to typed pointers")
    // 創建原生指針
    let rawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: aligment)

// 延遲釋放原生指針的內存
    defer {
        rawPointer.deallocate()
    }
    // 將原生指針綁定類型
    let typePointer = rawPointer.bindMemory(to: Int.self, capacity: count)
    typePointer.initialize(repeating: 0, count: count)
    defer {
        typePointer.deinitialize(count: count)
    }
    
    typePointer.pointee = 42
    typePointer.advanced(by: 1).pointee = 9
    typePointer.pointee
    typePointer.advanced(by: 1).pointee
    
    let bufferPointer = UnsafeBufferPointer(start: typePointer, count: count)
    for (index, value) in bufferPointer.enumerated() {
        print("value \(index): \(value)")
    }
}

原生指針轉換為類型指針,是通過調用內存綁定到特定的類型來完成的:

/// - Parameters:
    ///   - type: The type `T` to bind the memory to.
    ///   - count: The amount of memory to bind to type `T`, counted as instances
    ///     of `T`.
    /// - Returns: A typed pointer to the newly bound memory. The memory in this
    ///   region is bound to `T`, but has not been modified in any other way.
    ///   The number of bytes in this region is
    ///   `count * MemoryLayout<T>.stride`.
    public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafeMutablePointer<T>
  • type:數據類型
  • count:容量

通過對內存的綁定,我們可以通過類型安全的方法來訪問它。其實我們手動創建類型指針的時候,系統自動幫我們進行了內存綁定。

獲取一個實例的字節

這里定義了一個結構體 Sample來作為示例:

struct Sample {
    
    var number: Int
    var flag: Bool
    
    init(number: Int, flag: Bool) {
        self.number = number
        self.flag = flag
    }
}

do {
    print("Getting the bytes of an instance")
    
    var sample = Sample(number: 25, flag: true)
    // 1.
    withUnsafeBytes(of: &sample) { (rs) in
        
        for bute in rs {
            print(bute)
        }
    }
}

這里主要是使用了withUnsafeBytes 方法來實現獲取字節數:

/// - Parameters:
///   - arg: An instance to temporarily access through a raw buffer pointer.
///   - body: A closure that takes a raw buffer pointer to the bytes of `arg`
///     as its sole argument. If the closure has a return value, that value is
///     also used as the return value of the `withUnsafeBytes(of:_:)`
///     function. The buffer pointer argument is valid only for the duration
///     of the closure's execution.
/// - Returns: The return value, if any, of the `body` closure.
public func withUnsafeBytes<T, Result>(of arg: inout T, _ body: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result
  • arg:實例對象地址
  • body:回調閉包,參數為UnsafeRawBufferPointer 類型的指針

注意:該方法和回調閉包都有返回值,如果閉包有返回值,此返回值將會作為該方法的返回值;但是,一定不要在閉包中將body的參數,即:UnsafeRawBufferPointer 類型的指針作為返回值返回,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包。

withUnsafeBytes 同樣適合用 Array 和 Data 的實例.

使用指針的原則

不要從 withUnsafeBytes 中返回指針

絕對不要讓指針逃出 withUnsafeBytes(of:) 的作用域范圍。這樣的代碼會成為定時炸彈,你永遠不知道它什么時候可以用,而什么時候會崩潰。

一次只綁定一種類型

在使用 bindMemory方法將原生指針綁定內存類型,轉為類型指針的時候,一次只能綁定一個類型,例如:將一個原生指針綁定Int類型,不能再綁定Bool類型:

let typePointer = rawPointer.bindMemory(to: Int.self, capacity: count)
// 一定不要這么做
let typePointer1 = rawPointer.bindMemory(to: Bool.self, capacity: count)

但是,我們可以使用 withMemoryRebound 來對內存進行重新綁定。并且,這條規則也表明了不要將一個基本類型(如 Int)重新綁定到一個自定義類型(如 class)上。

/// - Parameters:
    ///   - type: The type to temporarily bind the memory referenced by this
    ///     pointer. The type `T` must be the same size and be layout compatible
    ///     with the pointer's `Pointee` type.
    ///   - count: The number of instances of `T` to bind to `type`.
    ///   - body: A closure that takes a mutable typed pointer to the
    ///     same memory as this pointer, only bound to type `T`. The closure's
    ///     pointer argument is valid only for the duration of the closure's
    ///     execution. If `body` has a return value, that value is also used as
    ///     the return value for the `withMemoryRebound(to:capacity:_:)` method.
    /// - Returns: The return value, if any, of the `body` closure parameter.
    public func withMemoryRebound<T, Result>(to type: T.Type, capacity count: Int, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
  • type:值的類型
  • count:值的個數
  • body:回調閉包,參數為UnsafeRawBufferPointer 類型的指針

注意:該方法和回調閉包都有返回值,如果閉包有返回值,此返回值將會作為該方法的返回值;但是,一定不要在閉包中將body的參數,即:UnsafeRawBufferPointer 類型的指針作為返回值返回,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包。

不要操作超出范圍的內存
do {
    
    let count = 3
    let stride = MemoryLayout<Int16>.stride
    let alignment = MemoryLayout<Int16>.alignment
    let byteCount = count * stride
    
    let pointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
    // 1. 這里的count+1,超出了原有指針pointer分配的內存范圍
    let bufferPointer = UnsafeRawBufferPointer.init(start: pointer, count: count + 1)
    
    for byte in bufferPointer {
        print(byte)
    }
}

這里的count+1,超出了原有指針pointer分配的內存范圍,切記不要出現這種情況。

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

推薦閱讀更多精彩內容