Swift 中的指針操作

ycue3627 7年前發布 | 6K 次閱讀 指針 Swift Apple Swift開發

默認情況下,Swift 是內存安全的,這意味著它禁止我們直接操作內存,并且確保所有的變量在使用前都已經被正確地初始化了。但是,Swift 也提供了我們使用指針直接操作內存的方法,直接操作內存是很危險的行為,很容易就出現錯誤,因此官方將直接操作內存稱為 “unsafe 特性”。

一旦我們開始直接操作內存,一切就得靠我們自己了,因為在這種情況下編譯能給我們提供的幫助實在不多。正常情況下,我們在與 C 進行交互,或者我們需要挖掘 Swift 內部實現原理的時候會需要使用到這個特性。

Memory Layout

Swift 提供了 MemoryLayout 來檢測特定類型的大小以及內存對齊大小:

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 2
MemoryLayout<Bool>.alignment  // return 2
MemoryLayout<Bool>.stride // return 2

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

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

MemoryLayout<Type> 是一個用于在編譯時計算出特定類型(Type)的 size, alignment 以及 stride 的泛型類型。返回的數值以字節為單位。例如 Int16 類型的大小為 2 個字節,內存對齊為 2 個字節以及當我們需要連續排列多個 Int16 類型時,每一個 Int16 所需要占用的大小(stride)為 2 個字節。所有基本類型的 stride 都與 size 是一致的。

接下來,看看結構體類型的 MemoryLayout:

structEmptyStruct{}

MemoryLayout<EmptyStruct>.size        // returns 0
MemoryLayout<EmptyStruct>.alignment   // returns 1
MemoryLayout<EmptyStruct>.stride  // returns 1

structSampleStruct{
    let number: UInt32
    let flag: Bool
}

MemoryLayout<SampleStruct>.size       // returns 5
MemoryLayout<SampleStruct>.alignment  // returns 4
MemoryLayout<SampleStruct>.stride // returns 8

空結構體的大小為 0,內存對齊為 1, 表明它可以存在于任何一個內存地址上。有趣的是 stride 為 1,這是因為盡管結構為空,但是當我們使用它創建一個實例的時候,它也必須要有一個唯一的地址。

對于 SampleStruct ,它所占的大小為 5,但是 stride 為 8。這是因為編譯需要為其填充空白的邊界,使其符合它的 4 字節內存邊界對齊。

再來看看類:

classEmptyClass{}

MemoryLayout<EmptyClass>.size     // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment    // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride       // returns 8 (on 64-bit)

classSampleClass{
    let number: Int64 = 0
    let flag: Bool = false
}

MemoryLayout<SampleClass>.size        // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.aligment    // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride  // returns 8 (on 64-bit)

由于類都是引用類型,所以它所有的大小都是 8 字節。

指針

一個指針就是對一個內存地址的封裝。在 Swift 當中直接操作指針的類型都有一個 “unsafe” 前綴,所以它的指針類型稱為 UnsafePointer 。這個前綴似乎看起來很令人惱火,不過這是 Swift 在提醒你,你現在正在跨越雷池,編譯器不會對這種操作進行檢查,你需要對自己的代碼承擔全部的責任。

Swift 中包含了一打類型的指針類型,每個類型都有它們的作用和目的,使用適當的指針類型可以防止錯誤的發生并且更清晰地表達開發者的意圖,防止未定義行為的產生。

Swift 的指針類型使用了很清晰的命名,我們可以通過名字知道這是一個什么類型的指針。可變或者不可變,原生(raw)或者有類型的,是否是緩沖(buffer)類型,這三種特性總共組合出了 8 種指針類型。

接下來的幾個小節會詳細介紹這幾種指針類型。

使用原生(Raw)指針

在 Playground 中添加如下代碼:

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

// 2
do {
  print("Raw pointers")

  // 3
  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  // 4
  defer {
    pointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }

  // 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("byte \(index): \(byte)")
  }
}

在這個代碼段中,我們使用了 Unsafe Swift 指針去存儲和讀取兩個整型數值。接下來是對這段代碼的解釋:

  1. 聲明了接下來都會用到的幾個常量:
    • count 表示了我們要存儲的整數的個數
    • stride 表示了 Int 類型的 stride
    • alignment 表示了 Int 類型的內存對齊
    • byteCount 表示占用的全部字節數
  2. 使用 do 來增加一個作用域,讓我們可以在接下的示例中復用作用域中的變量名
  3. 使用 UnsafeMutableRawPointer.allocate 方法來分配所需的字節數。我們使用了 UnsafeMutableRawPointer,它的名字表明這個指針可以用來讀取和存儲(改變)原生的字節。
  4. 使用 defer 來保證內存得到正確地釋放。操作指針的時候,所有內存都需要我們手動進行管理。
  5. storeBytes 和 load 方法用于存儲和讀取字節。第二個整型數值的地址通過對 pointer 的地址前進 stride 來得到。因為指針類型是 Strideable 的,我們也可以直接使用指針算術運算 (pointer+stride).storeBytes(of: 6, as: Int.self) 。
  6. UnsafeRawBufferPointer 類型以一系列字節的形式來讀取內存。這意味著我們可以這些字節進行迭代,對其使用下標,或者使用 filter , map 以及 reduce 這些很酷的方法。緩沖類型指針使用了原生指針進行初始化。

使用類型指針

我們可以使用類型指針實現跟上面代碼一樣的功能,并且更簡單:

do {
  print("Typed pointers")

  let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
  pointer.initialize(to: 0, count: count)
  defer {
    pointer.deinitialize(count: count)
    pointer.deallocate(capacity: count)
  }

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

注意到以下幾點不同:

  • 我們使用了 UnsafeMutablePointer.allocate 進行內存的分配。指定的泛型參數讓 Swift 知道我們將會使用這個指針來存儲和讀取 Int 類型的值。
  • 在使用類型指針前需要對其進行初始化,并在使用后銷毀。這兩個功能分別是使用 initialize 和 deinitialize 方法。
  • 類型指針提供了 pointee 屬性,它可以以類型安全的方式讀取和存儲值。
  • 當需要指針前進的時候,我們只需要指定想要前進的個數。類型指針會自動根據它所指向的數值類型來計算 stride 值。同樣的,我們可以直接對指針進行算術運算 (pointer + 1).pointee = 6 。
  • 有類型的緩沖型指針也會直接操作數值,而非字節

將原生指針轉換為類型指針

類型指針并不總是使用初始化得到的,它們可以從原生指針中轉化而來。

在 Playground 中添加如下代碼:

do {
  print("Converting raw pointers to typed pointers")

  let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  defer {
    rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }

  let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
  typedPointer.initialize(to: 0, count: count)
  defer {
    typedPointer.deinitialize(count: count)
  }

  typedPointer.pointee = 42
  typedPointer.advanced(by: 1).pointee = 6
  typedPointer.pointee
  typedPointer.advanced(by: 1).pointee

  let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}

這段代碼與上一段類似,除了它先創建了原生指針。我們通過將內存 綁定(binding) 到指定的類型上來創建類型指針。通過對內存的綁定,我們可以通過類型安全的方法來訪問它。將我們手動創建類型指針的時候,系統其實自動幫我們進行了內存綁定。

獲取一個實例的字節

很多時候我們需要從一個現存的實例里獲取它的字節。這時可以使用 withUnsafeBytes(of:) 方法。

在 Playground 中添加如下代碼:

do {
  print("Getting the bytes of an instance")

  var sampleStruct = SampleStruct(number: 25, flag: true)

  withUnsafeBytes(of: &sampleStruct) { bytes in
    for byte in bytes {
      print(byte)
    }
  }
}

這段代碼會打印出 SampleStruct 實例的原生字節。 withUnsafeBytes(of:) 方法可以獲取到 UnsafeRawBufferPointer 并傳入閉包中供我們使用。

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

使用 Swift 操作指針的三大原則

當我們使用 Swift 操作指針的時候必須加倍小心,防止寫出未定義行為的代碼。下面是幾個壞代碼的示例。

不要從 withUnsafeBytes 中返回指針

 // Rule #1
do {
  print("1. Don't return the pointer from withUnsafeBytes!")

  var sampleStruct = SampleStruct(number: 25, flag: true)

  let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
    return bytes // strange bugs here we come ??????
  }

  print("Horse is out of the barn!", bytes)  /// undefined !!!
}

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

一次只綁定一種類型

// Rule #2
do {
  print("2. Only bind to one type at a time!")

  let count = 3
  let stride = MemoryLayout<Int16>.stride
  let alignment = MemoryLayout<Int16>.alignment
  let byteCount =  count * stride

  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)

  let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)

  // Breakin' the Law... Breakin' the Law (Undefined behavior)
  let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)

  // If you must, do it this way:
  typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
    (boolPointer: UnsafeMutablePointer<Bool>) in
    print(boolPointer.pointee)  // See Rule #1, don't return the pointer
  }
}

**絕對不要**讓一個內存同時綁定兩個不同的類型。如果你需要臨時這么做,可以使用 `withMemoryRebound(to:capacity:)` 來對內存進行重新綁定。并且,這條規則也表明了不要將一個基本類型(如 Int)重新綁定到一個自定義類型(如 class)上。不要做這種傻事。

### 不要操作超出范圍的內存

```swift
// Rule #3... wait
do {
  print("3. Don't walk off the end... whoops!")

  let count = 3
  let stride = MemoryLayout<Int16>.stride
  let alignment = MemoryLayout<Int16>.alignment
  let byteCount =  count * stride

  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1????

  for byte in bufferPointer {
    print(byte)  // pawing through memory like an animal
  }
}

這是最糟糕的一種錯誤了,請再三檢查你的代碼,保證不要有這種情況出現。切記。

示例:隨機數生成

隨機數在很多地方都有重要的作用,從游戲到機器學習。macOS 提供了 arc4random 方法用于隨機數生成。不幸的是,這個方法無法在 Linux 上使用。并且, arc4random 方法只提供了 UInt32 類型的隨機數。事實上, /dev/urandom 這個設備文件中就提供了無限的隨機數。

這一小節中,我們將使用指針讀取這個文件,并產生完全類型安全的隨機數。

創建一個新 Playground,命名為 RandomNumbers ,并確保選擇了 macOS 平臺。

創建完成后,添加如下代碼:

import Foundation

enumRandomSource{

  static let file = fopen("/dev/urandom", "r")!
  static let queue = DispatchQueue(label: "random")

  static funcget(count: Int) -> [Int8] {
    let capacity = count + 1 // fgets adds null termination
    var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
    defer {
      data.deallocate(capacity: capacity)
    }
    queue.sync {
      fgets(data, Int32(capacity), file)
    }
    return Array(UnsafeMutableBufferPointer(start: data, count: count))
  }
}

為了確保整個系統中只存在一個 file 變量,我們對其使用了 static 修飾符。系統會在我們的進程結束時關閉文件。因為我們有可能在多個線程中同時獲取隨機數,所以需要使用一個串行的 GCD 隊列來進行保護。

get 函數是所有功能完成的地方。首先,我們根據傳入的大小分配了必要的內存,注意這里需要 +1 是因為 fets 函數總是以 \0 結束。接下來,我們就使用 fgets 函數從文件中讀取數據,確保我們在串行隊列中進行讀取操作。最后,我們先將數據封裝為一個 UnsafeMutableBufferPointer ,并將其轉化為一個數組。

在 playground 的最后添加如下代碼:

extensionInteger{

  static var randomized: Self {
    let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
    return numbers.withUnsafeBufferPointer { bufferPointer in
      return bufferPointer.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
        return $0.pointee
      }
    }
  }

}

Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized

這里我們為 Integer 協議添加了一個靜態屬性,并為其提供了默認實現。我們首先獲取了隨機數,隨后我們將獲得字節數組重新綁定為所需要的類型,然后返回它的值。簡單!

就這樣,我們使用 unsafe Swift 實現了一個類型安全的隨機器生成方法。

 

來自:http://www.swiftyper.com/2017/01/15/unsafe-swift/

 

 本文由用戶 ycue3627 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!