首页 > 解决方案 > 没有调试模式 UnsafePointer withMemoryRebound 将给出错误值

问题描述

在这里,我试图将 5 个字节连接到单个 Integer 值中,但我遇到了UnsafePointer withMemoryRebound方法问题。当我调试和检查日志时,它会给出正确的值。但是当我尝试不调试时,它会给出错误的值。(5 次错误值中有 4 次)。我对这个 API 感到困惑。我使用的方法是否正确?

情况1:

let data = [UInt8](rowData) // rowData is type of Data class
let totalKM_BitsArray = [data[8],data[7],data[6],data[5],data[4]]
self.totalKm = UnsafePointer(totalKM_BitsArray).withMemoryRebound(to:UInt64.self, capacity: 1) {$0.pointee}

案例2:

下面的代码适用于启用或禁用调试模式并给出正确的值。

    let byte0 : UInt64 = UInt64(data[4])<<64
    let byte1 : UInt64 = UInt64(data[5])<<32
    let byte2 : UInt64 = UInt64(data[6])<<16
    let byte3 : UInt64 = UInt64(data[7])<<8
    let byte4 : UInt64 = UInt64(data[8])
    self.totalKm = byte0 | byte1 | byte2 | byte3 | byte4

请建议我使用 UnsafePointer 的方式?为什么会出现这个问题?

附加信息:

let totalKm : UInt64 

let data = [UInt8](rowData)// 数据包含 [100, 200, 28, 155, 0, 0, 0, 26, 244, 0, 0, 0, 45, 69, 0, 0, 0, 4, 246]

let totalKM_BitsArray = [data[8],data[7],data[6],data[5],data[4]]// 包含 [ 244,26,0,0,0]

self.totalKm = UnsafePointer(totalKM_BitsArray).withMemoryRebound(to:UInt64.self, capacity: 1) {$0.pointee}

// 当打印日志给出正确的值时,当在设备上运行时给出错误的 3544649566089386 像这样。

 self.totalKm = byte0 | byte1 | byte2 | byte3 | byte4 

// 输出是 6900 这和预期的一样正确

标签: swiftbyte

解决方案


这种方法存在一些问题:

let data = [UInt8](rowData) // rowData is type of Data class
let totalKM_BitsArray = [data[8], data[7], data[6], data[5], data[4]]
self.totalKm = UnsafePointer(totalKM_BitsArray)
                 .withMemoryRebound(to:UInt64.self, capacity: 1) { $0.pointee }
  • 取消引用UnsafePointer(totalKM_BitsArray)未定义的行为,因为指向totalKM_BitsArray's 缓冲区的指针仅在初始化程序调用期间暂时有效(希望在未来的某个时候 Swift 会对此类构造发出警告)。

  • 您尝试仅绑定 5 个UInt8to实例UInt64,因此其余 3 个实例将是垃圾。

  • 您只能withMemoryRebound(_:)在大小和步幅相同的类型之间进行;UInt8和的情况并非如此UInt64

  • 这取决于您平台的字节顺序;data[8]将是小端平台上的最低有效字节,但大端平台上的最高有效字节。

您的位移实现避免了所有这些问题(并且通常是更安全的方法,因为您不必考虑布局兼容性、对齐和指针别名等问题)。

但是,假设您只想为最重要的字节用零填充数据,rowData[4]rowData[8]弥补其余的不太重要的字节,那么您将希望您的位移实现看起来像这样:

let rowData = Data([
  100, 200, 28, 155, 0, 0, 0, 26, 244, 0, 0, 0, 45, 69, 0, 0, 0, 4, 246
])

let byte0 = UInt64(rowData[4]) << 32
let byte1 = UInt64(rowData[5]) << 24
let byte2 = UInt64(rowData[6]) << 16
let byte3 = UInt64(rowData[7]) << 8
let byte4 = UInt64(rowData[8])
let totalKm = byte0 | byte1 | byte2 | byte3 | byte4
print(totalKm) // 6900

或者,迭代地:

var totalKm: UInt64 = 0
for byte in rowData[4 ... 8] {
  totalKm = (totalKm << 8) | UInt64(byte)
}
print(totalKm) // 6900

或者,使用reduce(_:_:)

let totalKm = rowData[4 ... 8].reduce(0 as UInt64) { accum, byte in
  (accum << 8) | UInt64(byte)
}
print(totalKm) // 6900

我们甚至可以将其抽象为一个扩展Data,以便更容易加载这些固定宽度的整数:

enum Endianness {
  case big, little
}

extension Data {
  /// Loads the type `I` from the buffer. If there aren't enough bytes to
  /// represent `I`, the most significant bits are padded with zeros.
  func load<I : FixedWidthInteger>(
    fromByteOffset offset: Int = 0, as type: I.Type, endianness: Endianness = .big
  ) -> I {
    let (wholeBytes, spareBits) = I.bitWidth.quotientAndRemainder(dividingBy: 8)
    let bytesToRead = Swift.min(count, spareBits == 0 ? wholeBytes : wholeBytes + 1)

    let range = startIndex + offset ..< startIndex + offset + bytesToRead
    let bytes: Data
    switch endianness {
    case .big:
      bytes = self[range]
    case .little:
      bytes = Data(self[range].reversed())
    }

    return bytes.reduce(0) { accum, byte in
      (accum << 8) | I(byte)
    }
  }
}

我们在这里做了一些额外的工作,以便我们读取正确的字节数,以及能够处理大端和小端。但是现在我们已经写好了,我们可以简单地写:

let totalKm = rowData[4 ... 8].load(as: UInt64.self)
print(totalKm) // 6900

请注意,到目前为止,我假设Data您得到的是零索引。这对于上述示例是安全的,但不一定安全,具体取决于数据的来源(因为它可能是一个切片)。您应该能够Data(someUnknownDataValue)获得可以使用的零索引数据值,尽管不幸的是我不相信有任何文档可以保证这一点。

为了确保您正确索引任意Data值,您可以定义以下扩展,以便在处理切片的情况下执行正确的偏移:

extension Data {
  subscript(offset offset: Int) -> Element {
    get { return self[startIndex + offset] }
    set { self[startIndex + offset] = newValue }
  }

  subscript<R : RangeExpression>(
    offset range: R
  ) -> SubSequence where R.Bound == Index {
    get {
      let concreteRange = range.relative(to: self)
      return self[startIndex + concreteRange.lowerBound ..<
                  startIndex + concreteRange.upperBound]
    }
    set {
      let concreteRange = range.relative(to: self)
      self[startIndex + concreteRange.lowerBound ..<
           startIndex + concreteRange.upperBound] = newValue
    }
  }
}

您可以使用它然后调用例如data[offset: 4]data[offset: 4 ... 8].load(as: UInt64.self)


最后值得注意的是,虽然您可以通过Data使用'方法将其实现为对位的重新解释withUnsafeBytes(_:)

let rowData = Data([
  100, 200, 28, 155, 0, 0, 0, 26, 244, 0, 0, 0, 45, 69, 0, 0, 0, 4, 246
])

let kmData = Data([0, 0, 0] + rowData[4 ... 8])
let totalKm = kmData.withUnsafeBytes { buffer in
  UInt64(bigEndian: buffer.load(as: UInt64.self))
}
print(totalKm) // 6900

这依赖于Data's 缓冲区是 64 位对齐的,这不能保证。尝试加载未对齐的值时会出现运行时错误,例如:

let data = Data([0x01, 0x02, 0x03])
let i = data[1...].withUnsafeBytes { buffer in
  buffer.load(as: UInt16.self) // Fatal error: load from misaligned raw pointer
}

通过加载单个UInt8值并执行位移,我们可以避免此类对齐问题(但是,如果/当UnsafeMutableRawPointer 支持未对齐加载时,这将不再是问题)。


推荐阅读