Swift 中動手封裝字節

chenxk168 9年前發布 | 9K 次閱讀 Swift Apple Swift開發

今天,我想嘗試封裝 Float32 類型數據到 SQLite 二進制大對象?Binary Large Object (BLOB)? 組中。當然,我可以使用 JSON,protobuf,或是其他一些編碼方式。除此之外, NSNumber , NSArray , NSCoder 和 plist 文件也是不錯的選擇。

不過,我希望以更加 Swift 的方式來實現,有點類似 C 語言風格,實現迅速且不會引入任何相關性,解碼器(decoder)也非常簡單,可以在任何平臺上實現。

PointerEncoder

我們將在 PointEncoder 結構體中實現最終的接口:

struct PointEncoder {
  // 解碼過程中,如果我們得到一個相當大的數值,可以假定為服務器拒絕或是損壞的數據
  static let MaxPoints = 1_310_719

  // 容量大小
  private static let _sizeOfCount = sizeof(Int64.self)

  // 一個點由兩個 Float32 類型數據組成,所占內存大小如下
  private static let _sizeOfPair = 2 * sizeof(Float32.self)

  static func encodePoints(points: [CGPoint]) -> NSData? {
  static func decodePoints(data: NSData) -> [CGPoint]
}

點數組的最大容量 MaxPoints 被限制在大約 10 MB,這已經遠遠滿足示例中的限定值。試想在移動蜂窩網絡或是信號不佳環境下的 WiFi,傳遞如此數量的點給服務器,會迫使服務器斷開連接。當然你可以視根據自身情況選擇合適的大小。

接下來,我們需要獲取以上類型所占內存大小。計算公式非常簡單,一旦明確了不同類型所占內存的大小,就能集中在一個地方定義它們,而不是分散在各地調用 sizeof() 函數。

編碼(encoding)

下面讓我們看看 encodePoints 的實現

guard !points.isEmpty && points.count < MaxPoints else { return nil }

// 緩存區的最大容量值
let bufferLength = _sizeOfCount + (points.count * _sizeOfPair)
precondition(bufferLength >= (_sizeOfCount + _sizeOfPair), "Empty buffer?")
precondition(bufferLength < megabytes(10), "Buffer would exceed 10MB")

第一步確保編碼內容不為空,且不超過容量最大值。

第二步計算緩存區的大小,不宜過大或過小。注意第一步中的 isEmpty 理論上來說排除了緩存區為空的可能,不過倘若之后有人重構了代碼,那就不一定了。緊接著我們會檢查緩存區分配過多內存的可能。

以上是我喜歡進行的額外安全檢查之一,主要考慮到一些二把刀程序員的尿性。試想之后有人了重構代碼,并意外引入一個錯誤,但優秀的程序員不太可能會刪除 precondition 斷言語句。 precondition 語句之后緊跟著是分配內存,請時刻注意“這里可能發生危險,要格外小心!”。

let rawMemory = UnsafeMutablePointer<Void>.alloc(bufferLength)

// Failed to allocate memory
guard rawMemory != nil else { return nil }

下一步開始真正創建緩存區,一旦創建失敗就跳出。

控制程序應對內存不足的情況非常困難。如果是因為內存不足造成創建類實例失敗,程序應該調用 abort() 方法,因為簡單的日志輸出或 print 語句依舊涉及一些內存分配操作,這就無法以日志的形式通知失敗結果(會使得所有的構造方法失敗)。

考慮另外一種情況,分配大的緩存區有會可能失敗,但堆碎片(heap fragmentation)可能還存在額外可用的內存。因此,如何優雅地處理它是一門學問(尤其像在 iOS 這種受限的環境中)。

UnsafeMutablePointer<Int64>(rawMemory).memory = Int64(points.count)

let buffer = UnsafeMutablePointer<Float32>(rawMemory + sizeOfCount)

這里有一點要注意。等式右邊將 points.count 類型轉換成了 64 位類型的整數,因此不隨平臺變化而發生改變(Swift 的 Int 類型在編譯時會自適應平臺,32 位平臺下為 32 位整數,同理 64 位平臺下為 64 位整數)。我們可不希望用戶在升級設備后,引發崩潰或數據損壞問題。

等式左側將 rawMemory 強轉成 Int64 指針類型,然后將其指向的內存內容賦值為 Int64(points.count) 。64 位整數占 8 個字節,因此分配的前 8 個字節內存包含了點個數( sizeOfCount )信息。

最后,我們將指針偏移 8 個字節(正如前面所說的),指針指向緩存區首地址。

for (index, point) in points.enumerate() {
  let ptr = buffer + (index * 2)

  // Store the point values.
  ptr.memory = Float32(point.x)
  ptr.advancedBy(1).memory = Float32(point.y)
}

接下來進行遍歷 points 點數組操作。我們對 UnsafeMutablePointer 指針進行簡單偏移量計算,得到緩沖區中的相關位置。值得注意的是,swift 中的不安全指針僅知道當前所使用的類型大小,所有指針偏移量都是以當前類型為單位,而非字節!(不過 Void 類型指針無法確定類型的大小,所以這種情況是以字節為單位的)。

因此,通過對基址進行 index * 2 偏移累加,得到下一對點成員(注:即x,y點坐標)的地址。然后我們為當前指針指向的內存區域作賦值操作。

接著我使用了 ptr.advancedBy() 方法,并未保留指針的引用,同時也沒有設置 ptr 為可變指針。這僅僅是我個人喜好。你可以使用 + 或 advancedBy() ,這兩者作用一致。

return NSData(
  bytesNoCopy: rawMemory, 
  length: bufferLength, 
  deallocator: { (ptr, length) in
    ptr.destroy(length)
    ptr.dealloc(length)
})

最后要注意的,我們將數據返回給調用者。此時已經分配了一個合適的緩存區,接著使用 bytesNoCopy 進行初始化操作,將適當的長度以及閉包作為參數傳遞給函數。

為什么要傳遞一個用作釋放的閉包參數(deallocator)呢?從技術上講,你或許可以使用 NSData(bytesNoCopy:length:freeWhenDone:) 僥幸實現,但無法保證沒有意外發生。倘若 Swift runtime 沒有使用系統默認的 malloc/free 方法,而是采用其他內存分配方式,那么你將得到一個報錯。

如果我們的緩存區恰巧需要存儲一些復雜的 Swift 類型,適時的釋放操作是必須的:你必須調用 ptr.destroy(count) 來進行釋放,需要借助引用類型,遞歸枚舉用例等等,否則會造成內存泄露。在本例中,我們知道 Float32 和 Int64 類型所占位數,從技術正確角度來講,調用 destroy 方法能夠更好的保證這一點。

解碼(decoding)

guard
  data.bytes != nil &&
    data.length > (_sizeOfCount + _sizeOfPair)
  else { return [] }

首先,我們確保 NSData 中的指針不為 nil ,并且足夠容納 Int64 數量的點數組。這為接下來的操作鋪平了道路,不需要再進行一些額外的安全檢查。

let rawMemory = data.bytes
let buffer = rawMemory + _sizeOfCount

// 從內存中獲取到 Int64 類型的點個數
let pointCount64 = UnsafePointer<Int64>(rawMemory).memory

precondition(
  Int64(MaxPoints) < Int64(Int32.max),
  "MaxPoints would overflow on 32-bit platforms")
precondition(
  pointCount64 > 0 && pointCount64 < Int64(MaxPoints),
  "Invalid pointCount = \(pointCount64)")

let pointCount = Int(pointCount64)

接下來設置我們的指針。再次將原始指針強制轉換成 Int64 類型的指針,此時我們使用了非可變指針,這是出于只讀操作的考慮。

注意到前面代碼中我將點個數類型設置為 64 位,這樣確保了 Int32.max 不會溢出或下溢;C 語言中經常使用 if(value + x > INT_MAX) 判斷檢查是否溢出,屬于未定義行為之一。現在請放下手上工作思考一分鐘:計算機是如何處理 value + x 超出整型最大值的情況呢?答案是:無法繼續累加,轉而變成一個負值。那么當我們使用一個超大的負值進行類似 malloc 或 is_admin() 操作時會發生什么情況呢?這是我留給讀者的一個課后小作業。

末行代碼將點個數轉換成 Int 類型。 32 位平臺上一旦值超過 Int32.max ,我們將會陷入“萬劫不復”。Swift 相對于 C 語言要安全的多 —— 我們必須時刻警惕值溢出或下溢的情況發生。一旦此類情況發生,程序就會在運行時崩潰,值得慶幸的是,程序在掛掉之前會給出清晰的錯誤提示。

64 位平臺上,絕對有可能超過 4GB 容量點數組的情況(數值超過大約42億),代碼需要進一步重構。不過對于我的需求來說無關緊要,所以這里采用了硬編碼限制了容量。這也使得在 64 位系統上創建的值無法加載到 32 位系統當中(這只是理論上最大值的情況,實際我所使用的容量將會小得多)。

var points: [CGPoint] = []
points.reserveCapacity(pointCount)

for ptr in (0..<pointCount).map({
  UnsafePointer<Float32>(buffer) + (2 * $0)
}) {
  points.append(
    CGPoint(
      x: CGFloat(ptr.memory),
      y: CGFloat(ptr.advancedBy(1).memory))
  )
}

return points

代碼也很簡單。我們設定數組的備用容量,以避免重新分配。這不會對性能造成太大影響,畢竟我們已經知道了最大限制容量,所以這么做沒什么問題。

另外,指針類型為 Float32 ,Swift 知道該類型所占內存大小。我們只需要將索引值乘以2( 2 * $0 )得到下一對坐標點的指針,然后從指針指向的內存區域讀取數值。

關于測試

毫無疑問,類似這種類型都應該使用 Address Sanitizer 內存檢測用具來幫助捕獲任何濫用內存的問題,并且在產品發布前應該進行大量的代碼審查(或借助 AFL fuzzing 同樣能夠方便揭露一些問題)。

我從來不敢 100% 保證代碼中任何有關線程或內存的部分不會出現紕漏。我甚至無法 100% 確定本文用例沒有 bug。不過我使用 Addess Sanitizer 工具并沒有發現任何問題,但我堅信一個好的程序員應該有敬畏之心。時刻警惕那些可能出現的錯誤或失誤(如果你發現本文有任何紕漏,請留言告知我!)

包括你在內,沒有人優秀到寫代碼可以完全避免緩沖區溢出。

總結

Swift 編譯器始終重視安全問題,但它有時也令人心寒。如果你保證不做一些調皮的事情,它會完全信任你。如果你有必要做一些字節或 void 指針操作,請重新創建一個 .swift 文件然后在里面使用。

最終實現

我已經在最終實現的用例 gist 中嵌入了要點和詳細注釋。如果對你有幫助的話,請盡情使用它。

// Written by Russ Bishop
// MIT licensed, use freely.
// No warranty, not suitable for any purpose. Use at your own risk!

struct PointEncoder {
  // When parsing if we get a wildly large value we can
  // assume denial of service or corrupt data.
  static let MaxPoints = 1_310_719

  // How big an Int64 is
  private static let _sizeOfCount = sizeof(Int64.self)

  // How big a point (two Float32s are)
  private static let _sizeOfPair = 2 * sizeof(Float32.self)


  static func encodePoints(points: [CGPoint]) -> NSData? {
    guard !points.isEmpty && points.count < MaxPoints else { return nil }

    // Total size of the buffer
    let bufferLength = _sizeOfCount + (points.count * _sizeOfPair)
    precondition(bufferLength >= (_sizeOfCount + _sizeOfPair), "Empty buffer?")
    precondition(bufferLength < megabytes(10), "Buffer would exceed 10MB")

    let rawMemory = UnsafeMutablePointer<Void>.alloc(bufferLength)

    // Failed to allocate memory
    guard rawMemory != nil else { return nil }

    // Store the point count in the first portion of the buffer
    UnsafeMutablePointer<Int64>(rawMemory).memory = Int64(points.count)

    // The remaining bytes are for the Float32 pairs
    let buffer = UnsafeMutablePointer<Float32>(rawMemory + _sizeOfCount)

    // Store the points
    for (index, point) in points.enumerate() {
      // Since buffer is UnsafeMutablePointer<Float32>, addition counts
      // the number of Float32s, *not* the number of bytes!
      let ptr = buffer + (index * 2)

      // Store the point values.
      ptr.memory = Float32(point.x)
      ptr.advancedBy(1).memory = Float32(point.y)
    }

    // We can tell NSData not to bother copying memory.
    // For consistency and since we can't guarantee the memory allocated
    // by UnsafeMutablePointer can just be freed, we provide a deallocator
    // block. 
    return NSData(
      bytesNoCopy: rawMemory,
      length: bufferLength,
      deallocator: { (ptr, length) in
        // If ptr held more complex types, failing to call
        // destroy will cause lots of leakage.
        // No one wants leakage.
        ptr.destroy(length)
        ptr.dealloc(length)
    })
  }

  static func decodePoints(data: NSData) -> [CGPoint] {
    // If we don't have at least one point pair
    // and a size byte, bail.
    guard
      data.bytes != nil &&
        data.length > (_sizeOfCount + _sizeOfPair)
      else { return [] }

    let rawMemory = data.bytes
    let buffer = rawMemory + _sizeOfCount

    // Extract the point count as an Int64
    let pointCount64 = UnsafePointer<Int64>(rawMemory).memory

    // Swift is safer than C here; you can't
    // accidentally overflow/underflow and not
    // trigger a trap, but I am still checking
    // to provide better error messages.
    // In all cases, better to kill the process
    // than corrupt memory.
    precondition(
      Int64(MaxPoints) < Int64(Int32.max),
      "MaxPoints would overflow on 32-bit platforms")
    precondition(
      pointCount64 > 0 && pointCount64 < Int64(MaxPoints),
      "Invalid pointCount = \(pointCount64)")

    // On 32-bit systems this would trap if
    // MaxPoints were too big and we didn't
    // check above.
    let pointCount = Int(pointCount64)
    precondition(
      _sizeOfPair + (_sizeOfCount * pointCount) <= data.length,
      "Size lied or buffer truncated")

    var points: [CGPoint] = []
    // Small optimization since
    // we know the array size
    points.reserveCapacity(pointCount)

    for ptr in (0..<pointCount).map({
      // buffer points past the size header
      // Again, since the pointer knows we are
      // counting Float32 values we want the
      // number of Float32s, *not* their size
      // in bytes!
      UnsafePointer<Float32>(buffer) + (2 * $0)
    }) {
      points.append(
        CGPoint(
          x: CGFloat(ptr.memory),
          y: CGFloat(ptr.advancedBy(1).memory))
      )
    }

    return points
  }
}

func kilobytes(value: Int) -> Int {
  return value * 1024
}

func megabytes(value: Int) -> Int {
  return kilobytes(value * 1024)
}

func gigabytes(value: Int) -> Int {
  return megabytes(value * 1024)
}

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問http://swift.gg。

 

來自:http://swift.gg/2016/09/01/packing-bytes-in-swift/

 

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