Swift结构体转换为NSData并返回

28

我有一个包含结构体和NSObject的结构体,我想将其序列化为NSData对象:

struct Packet {
  var name: String
  var index: Int
  var numberOfPackets: Int
  var data: NSData
}

var thePacket = Packet(name: name, index: i, numberOfPackets: numberOfPackets, data: packetData)

如何最好地将数据包序列化为NSData,以及如何最好地反序列化它?

使用

var bufferData = NSData(bytes: & thePacket, length: sizeof(Packet))

of仅会给我name和data的指针。我正在探索NSKeyedArchiver,但那样我就得把Packet变成一个对象,而我更喜欢它是一个结构体。

Cheers

Nik


这可能会有所帮助:https://github.com/x43x61x69/Struct-to-NSData-and-Back-Examples - Ian
很遗憾,他的程序出了点问题,不是保存字符串,而是保存了内存地址。因此,当他读取并引用该内容时,字符串依然在内存中。但实际上,他从未保存字符串的内容! - niklassaers
这个方法怎么样:https://gist.github.com/nubbel/5b0a5cb2bf6a2e353061? - Lukáš Kubánek
5个回答

14

没有真正得到任何反馈,这是我最终得出的解决方案:

  1. 为我的结构体编写 encode()decode() 函数
  2. Int 更改为 Int64,以便在32位和64位平台上 Int 具有相同的大小
  3. 拥有一个中间结构体(ArchivedPacket),它没有字符串或 Data,而只有 Int64

这是我的代码,如果有更简便的方法,请提供您的反馈,谢谢!

public struct Packet {
    var name: String
    var index: Int64
    var numberOfPackets: Int64
    var data: NSData

    struct ArchivedPacket {
        var index : Int64
        var numberOfPackets : Int64
        var nameLength : Int64
        var dataLength : Int64
    }

    func archive() -> NSData {

        var archivedPacket = ArchivedPacket(index: Int64(self.index), numberOfPackets: Int64(self.numberOfPackets), nameLength: Int64(self.name.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)), dataLength: Int64(self.data.length))

        var metadata = NSData(
            bytes: &archivedPacket,
            length: sizeof(ArchivedPacket)
        )

        let archivedData = NSMutableData(data: metadata)
        archivedData.appendData(name.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!)
        archivedData.appendData(data)

        return archivedData
    }

    func unarchive(data: NSData!) -> Packet {
        var archivedPacket = ArchivedPacket(index: 0, numberOfPackets: 0, nameLength: 0, dataLength: 0)
        let archivedStructLength = sizeof(ArchivedPacket)

        let archivedData = data.subdataWithRange(NSMakeRange(0, archivedStructLength))
        archivedData.getBytes(&archivedPacket)

        let nameRange = NSMakeRange(archivedStructLength, Int(archivedPacket.nameLength))
        let dataRange = NSMakeRange(archivedStructLength + Int(archivedPacket.nameLength), Int(archivedPacket.dataLength))

        let nameData = data.subdataWithRange(nameRange)
        let name = NSString(data: nameData, encoding: NSUTF8StringEncoding) as! String
        let theData = data.subdataWithRange(dataRange)

        let packet = Packet(name: name, index: archivedPacket.index, numberOfPackets: archivedPacket.numberOfPackets, data: theData)

        return packet
    }
}

11

Swift 5

如果你在使用苹果平台,现在可以使用 Codable 。请参阅文档

Swift 3

这是从 Xcode 8.2.1 中的 Playground 中复制粘贴的内容,可以正常工作。与其他答案相比,它要简单一些。

import Foundation

enum WhizzoKind {
    case floom
    case bzzz
}

struct Whizzo {
    let name: String
    let num: Int
    let kind:WhizzoKind

    static func archive(w:Whizzo) -> Data {
        var fw = w
        return Data(bytes: &fw, count: MemoryLayout<Whizzo>.stride)
    }

    static func unarchive(d:Data) -> Whizzo {
        guard d.count == MemoryLayout<Whizzo>.stride else {
            fatalError("BOOM!")
        }

        var w:Whizzo?
        d.withUnsafeBytes({(bytes: UnsafePointer<Whizzo>)->Void in
            w = UnsafePointer<Whizzo>(bytes).pointee
        })
        return w!
    }
}

let thing = Whizzo(name:"Bob", num:77, kind:.bzzz)
print("thing = \(thing)")
let dataThing = Whizzo.archive(w: thing)
let convertedThing = Whizzo.unarchive(d: dataThing)
print("convertedThing = \(convertedThing)")

注释

我无法将archiveunarchive实例方法制作成,因为Data.init(bytes: count:)对于bytes参数是可变的。而self不可变,所以...这对我来说毫无意义。

WhizzoKind枚举是存在的,因为那是我关心的内容。它对于示例并不重要。有些人可能像我一样对枚举很担心。

我必须从其他4个 SO 问题/答案中拼凑出这个答案:

以及这些文档: - http://swiftdoc.org/v3.1/type/UnsafePointer/

并且冥想 Swift 闭包语法,直到我想尖叫。

所以感谢那些其他 SO 提问者/作者。

更新

所以这将不能在不同设备之间工作。例如,从 iPhone 7 发送到 Apple Watch。因为 stride 不同。上面的示例在 iPhone 7 模拟器上是 80 字节,但在 Apple Watch Series 2 模拟器上只有 40 字节。

看起来 @niklassaers 的方法(而不是语法)仍然是唯一可行的方法。我会将此答案保留在这里,因为它可能会帮助其他人了解所有围绕此主题的新 Swift 3 语法和 API 更改。

我们唯一的真正希望是这个 Swift 提案:https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md


这甚至在同一设备上都不会起作用,我认为。它只是在你的示例中偶然起作用,因为“name”字符串被保存在内存中。你实际上没有在这里序列化字符串。 - Dag Ågren
@DagÅgren,我会在一个完整的项目中进行不同于Playground的测试来验证你的说法。 - Jeff

7
基本结构对象最容易使用的方法是 PropertyListEncoder & PropertyListDecoder
这是示例代码; Swift 5
struct Packet: Codable {
   var name: String
   var index: Int
   var numberOfPackets: Int
   var data: Data
}

func getDataFromPacket(packet: Packet) -> Data?{
  do{
    let data = try PropertyListEncoder.init().encode(packet)
    return data
  }catch let error as NSError{
    print(error.localizedDescription)
  }
    return nil
}

func getPacketFromData(data: Data) -> Packet?{
    do{
      let packet = try PropertyListDecoder.init().decode(Packet.self, from: data)
      return packet
    }catch let error as NSError{
      print(error.localizedDescription)
    }
    
    return nil
}

4

我使用Jeff的示例创建了以下结构:

struct Series {
    
    var name: String?
    var season: String?
    var episode: String?
    
    init(name: String?, season: String?, episode: String?) {
        self.name = name
        self.season = season
        self.episode = episode
    }
    
    static func archive(w: Series) -> Data {
        var fw = w
        return Data(bytes: &fw, count: MemoryLayout<Series>.stride)
    }
    
    static func unarchive(d: Data) -> Series {
        guard d.count == MemoryLayout<Series>.stride else {
            fatalError("Error!")
        }
        
        var w: Series?
        d.withUnsafeBytes({(bytes: UnsafePointer<Series>) -> Void in
            w = UnsafePointer<Series>(bytes).pointee
        })
        return w!
    }
}

正如Dag所提到的,整个事情有点脆弱。当名称包含空格或下划线时,应用程序有时会崩溃,有时候则是无缘无故地崩溃。在所有情况下,未归档的名称看起来类似于'4\200a\256'。令人惊讶的是,在季节或剧集的情况下(例如“第二季”),这不是问题。这里的空格不会导致应用程序崩溃。

也许将字符串编码为utf8是一种替代方法,但我对存档/取消存档方法不太熟悉,无法针对此情况进行调整。


这对我来说几乎可以工作。我创建了一些结构体,滑出应用程序,再次进入,它就在那里,但如果我再次滑出并点击应用程序,它就会崩溃。有什么想法吗? - maxcodes
嗨Max,我停止了对这个主题的工作,因为我读到Swift 4将更容易地将结构体(至少是字符串)转换为NSData。对于我的项目,我决定使用JSON将我的数据转换为资产并将其存储在iCloud中。 - Martin

1

这似乎是最近发布的,对我来说看起来很可靠。还没有尝试过...

https://github.com/a2/MessagePack.swift


嗯,如果你想要的是一种神奇的序列化方法,那么Swift并没有这样的方法。自从C语言的好日子以来,当你有一个带指针的结构体时,这表示你不能序列化该结构体实例的字节,而不跟随指针并获取它们的数据。Swift同样适用。

根据您的序列化需求和约束条件,我建议使用NSCoding甚至JSON字符串来整理您的代码,并使其更可预测。当然,您需要编写映射器,这会增加一些开销。每个人都会告诉你:“先测量一下”。

现在,这里是有趣的部分:

如果你真的想要在该结构体中内联数据,并且像你正在做的那样在构建数据包时不围绕NSData,而是流式传输内容,你可以使用Swift Tuples来保留字节,这与您在C中使用char [CONST]保留字节的方式非常相似:

struct what { 
    var x = 3 
}    

sizeof(what)

$R0: Int = 8

struct the { 
    var y = (3, 4, 5, 7, 8, 9, 33) 
}    

sizeof(the)

$R1: Int = 56

为了更详细地解释一下,我认为这很糟糕,但是有可能。您可以写入元组的内存位置并从中读取使用类似于此的内容

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接