【问题标题】:Storing the data representations of multiple, differently typed objects in a single Data instance在单个 Data 实例中存储多个不同类型对象的数据表示
【发布时间】:2020-11-03 18:52:00
【问题描述】:

动机

据我所知,Data 是一个抽象字节缓冲区的结构。它引用内存中的物理区域,换句话说:连续的字节数。现在我想在内存中有效地存储多个值(作为原始数据),其中的值并非都是同一类型

我在这里对高效的定义 ≔ 存储所有这些值,没有任何未使用的缓冲区/间隙字节。

将原始数据存储在内存中

let a: UInt8 = 39
let b: Int32 = -20001
let string: String = "How awesome is this data?!"

现在我想将所有这些值的数据顺序存储在内存中,没有任何类型信息。

let data = [a.asData, b.asData, string.asData].concatenated()

假设.asData 属性将每个实例的字节表示检索为[UInt8] 数组,然后将它们包装在Data 实例中。 concetenated() 方法然后将这 3 个 Data 实例连接到单个 Data 实例,如下所示:

extension Collection where Element == Data {
    func concatenated() -> Data {
        reduce(into: Data()) { (result, nextDataChunk) in
            result.append(nextDataChunk)
        }
    }
}

从内存中读回数据到各自的类型中

假设这一切都很好,我现在有一个 Data 实例,我想从中恢复 3 个原始值(及其原始类型)。我就是这样做的:

var cursor = 0

let a: UInt8 = data.withUnsafeBytes { pointer in
    pointer.load(fromByteOffset: cursor, as: UInt8.self)
}
cursor += MemoryLayout<UInt8>.size // +1

let b: Int32 = data.withUnsafeBytes { pointer in
    pointer.load(fromByteOffset: cursor, as: Int32.self)
}
cursor += MemoryLayout<Int32>.size // +4

let string: String = data.withUnsafeBytes { pointer in
    pointer.load(fromByteOffset: cursor, as: String.self)
}
cursor += MemoryLayout<String>.size // +16

问题

问题是这会引发运行时错误:

致命错误:从未对齐的原始指针加载

我知道为什么:

Int32 的对齐方式为 4(因为它有 4 个字节长)。换句话说:当使用原始指针读取数据时,Int32 的第一个字节必须位于 4 的倍数的索引处。但由于第一个值仅是 UInt8,@ 的数据字节987654336@ 从索引 1 开始,不是 4 的倍数。因此,我得到了错误。


我的问题是这样的:

  • 我能否以某种方式使用代表不同类型实例的原始Data 来重新创建此类实例而不会出现对齐错误?怎么样?

  • 如果这是不可能的,有没有办法在连接它们时自动正确对齐Data 块?

【问题讨论】:

    标签: ios swift pointers nsdata foundation


    【解决方案1】:

    关于未对齐数据的问题是您需要使用 Data 的 subdata 方法。除此之外,您还可以创建一些帮助程序以使您的生活更轻松,如下所示:

    这会将任何数字类型转换为数据:

    extension Numeric {
        var data: Data {
            var bytes = self
            return .init(bytes: &bytes, count: MemoryLayout<Self>.size)
        }
    }
    

    这会将任何符合字符串协议的类型转换为数据(字符串/子字符串)

    extension StringProtocol {
        var data: Data { .init(utf8) }
    }
    

    这会将任何有效的 utf8 编码字节序列 (UInt8) 转换为字符串

    extension DataProtocol {
        var string: String? { String(bytes: self, encoding: .utf8) }
    }
    

    这是一种将字节转换为对象或对象集合(数组)的通用方法:

    extension ContiguousBytes {
        func object<T>() -> T { withUnsafeBytes { $0.load(as: T.self) } }
        func objects<T>() -> [T] { withUnsafeBytes { .init($0.bindMemory(to: T.self)) } }
    }
    

    以及用于连接数据数组的简化通用版本:

    extension Collection where Element == DataProtocol {
        var data: Data { .init(joined()) }
    }
    

    用法:

    let a: UInt8 = 39
    let b: Int32 = -20001
    let string: String = "How awesome is this data?!"
    let data = [a.data, b.data, string.data].data
    
    // just set the cursor (index) at the start position
    var cursor = data.startIndex
    // get the subdata from that position onwards
    let loadedA: UInt8 = data.subdata(in: cursor..<data.endIndex).object()  // 39
    // advance your cursor for the next position
    cursor = cursor.advanced(by: MemoryLayout<UInt8>.size)
    // get your next object
    let loadedB: Int32 = data.subdata(in: cursor..<data.endIndex).object()  // -20001
    // advance your position to the start of the string data
    cursor = cursor.advanced(by: MemoryLayout<Int32>.size)
    // load the subdata as string
    let loadedString = data.subdata(in: cursor..<data.endIndex).string  // "How awesome is this data?!"
    

    编辑/更新: 当然,加载字符串只起作用,因为它位于字节集合的末尾,否则你需要使用 8 个字节来存储它的大小:

    let a: UInt8 = 39
    let b: Int32 = -20001
    let string: String = "How awesome is this data?!"
    let c: Int = .max
    let data = [a.data, b.data, string.count.data, string.data, c.data].data
    
    var cursor = data.startIndex
    let loadedA: UInt8 = data.subdata(in: cursor..<data.endIndex).object()  // 39
    print(loadedA)
    cursor = cursor.advanced(by: MemoryLayout<UInt8>.size)
    let loadedB: Int32 = data.subdata(in: cursor..<data.endIndex).object()  // -20001
    print(loadedB)
    cursor = cursor.advanced(by: MemoryLayout<Int32>.size)
    let stringCount: Int = data.subdata(in: cursor..<data.endIndex).object()
    print(stringCount)
    cursor = cursor.advanced(by: MemoryLayout<Int>.size)
    let stringEnd = cursor.advanced(by: stringCount)
    
    if let loadedString = data.subdata(in: cursor..<stringEnd).string {  // "How awesome is this data?!"
        print(loadedString)
        cursor = stringEnd
        let loadedC: Int = data.subdata(in: cursor..<data.endIndex).object()  // 9223372036854775807
        print(loadedC)
    }
    

    这会打印出来

    39
    -20001
    26
    这些数据有多棒?!
    9223372036854775807

    【讨论】:

    • 你是对的,关键是为每个数据块创建新的Data 实例以逃避对齐问题。我希望找到一种方法来使用相同的通用方法访问任何原始类型的原始字节,但似乎我不能这样做? (即,对于Strings 和Numerics,我需要不同的data 实现。)有没有办法知道String 的大小而不用存储它?你知道为什么MemorySize&lt;String&gt; 在字符串的大小实际上是灵活的时候总是返回一个 16 字节的固定值吗?
    • 编码String 的方法有很多种,.utf8 是其中之一,但它是唯一一种永远不会失败的方法。所有其他编码选项将返回一个可选项。关于大小String 是一个集合,因此无法预测其大小。 AFAIK 只有那些具有固定大小的类型才能加载,而无需设置结束索引。
    • 数字类型的编码和解码也有字节顺序。如果您需要对Date 进行编码/解码,可以查看此post
    • 顺便说一句,您可能对此相关post 感兴趣
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多