问题描述
动机
据我所知,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
,因此数据Int32
的字节从索引1开始,这不是4的倍数。因此,我得到了错误。
我的问题是这样:
-
我可以以某种方式使用表示不同类型实例的原始
Data
来重新创建此类实例而不会出现对齐错误吗?怎么样? -
如果这不可能,那么首先将
Data
块连接起来时,是否有办法自动正确对齐它们?
解决方法
关于数据未对齐的问题是,您需要使用Data的子数据方法。除此之外,您还可以创建一些助手来使您的生活更轻松,如下所示:
这会将任何数字类型转换为数据:
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,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