iOS中的本机对象序列化

NSJSONSerialization仅提供第二部分-从NSDictionaty / NSArray到JSON的序列化,因此您仍然需要执行从自定义对象到NSDictionary的映射。 该类很简单,应该像这样使用:

  NSDictionary * objectDict = @ { 
@“姓名”:@“约翰·史密斯”,
@“年龄”:@(34),
@“性别”:@“男性”
};
NSError *错误;
NSData * jsonData = [NSJSON序列化dataWithJSONObject:objectDict选项:kNilOptions错误:&error];

正如我提到的,我们旨在编码的树的根对象只能是NSDictionaty或NSArray。 但是使用NSJSONSerialization还有其他一些限制:

  • 所有对象都是NSString,NSNumber,NSArray,NSDictionary或NSNull的实例。
  • 所有字典键都是NSString的实例。
  • 数字不是NaN或无穷大。

您可以将一些选项传递给NSJSONSerialization来对数据进行编码和解码:

1.编码(NSJSONWritingOptions):

  • PrettyPrinted-使用空格和缩进使输出更具可读性(在结果数据中添加一些额外的字节)
  • SortedKeys —按字典顺序对键进行排序

2.解码(NSJSONReadingOptions):

  • MutableContainers —数组和字典被创建为可变对象
  • MutableLeaves-字符串创建为NSMutableString的实例。
  • AllowFragments-告诉解析器允许不是NSArray或NSDictionary实例的顶级对象。

如果您不想指定任何选项,则可以传递kNilOptions -ObjC代码中的常量或Swift中的空数组。

回到我们的序列化链:使用NSJSONSerialization,我们仍然需要将自定义对象映射到数组和字典中。 为此,您有时会在模型对象内使用手动键值映射:

  -(NSDictionary *)jsonDict { 
返回@ {
@“名称”:self.name,
@“年龄”:@(self.age),
@“性别”:self.gender
};
}

但是也可以使用ObjC运行时中的类检查来使其自动化。

  #import  
  ... 
  unsigned int outCount,我; 
  objc_property_t * properties = class_copyPropertyList([self class],&outCount); 
  for(i = 0; i <outCount; i ++){ 
objc_property_t属性=属性[i];
fprintf(stdout,“%s%s \ n”,property_getName(property),property_getAttributes(property));
}
 免费(属性); 

在这里,我们获得给定类的所有属性的列表,其中包括一些属性,包括属性类型。 因此,我们可以概括该算法以将每个对象序列化为JSON数据。 如果对象属性的确切键或值不是我们想要在JSON中看到的值,则可以选择使用一些映射表和转换。 使用此方法可以成功地使用许多库(使用JSONModel时我获得了令人愉快的经历)。

列表

Plist(来自“属性列表”)是一种结构化的方式,用于表示和保留单个对象或对象树。 该格式已出现在NeXTSTEP操作系统中,并在数十年间发展为现代macOS / iOS生态系统。

通常,Foundation世界中的plist序列化看起来像JSON序列化。 自MacOS 10.2 / iOS 2起,我们就有了NSPropertyListSerialization(从Swift调用时为PropertyListSerialization)。该类(NSJSONSerialization的哥哥)在plist(由NSDictionary或NSArray实例表示在内存中)和二进制数据之间进行转换。 (由NSData表示)。

  NSError *错误; 
NSPropertyListFormat格式;
id plist = [NSPropertyListSerialization propertyListWithData:数据选项:NSPropertyListImmutable格式:&format错误:&error];

NSPropertyListSerialization仅适用于有限数量的Foundation类: NSData,NSString,NSArray,NSDictionary,NSDate和NSNumber。 如果某个其他类的实例位于字典或数组中,则您要编码的序列化将失败。

可以使用两种格式对属性列表进行编码/解码:xml( NSPropertyListXMLFormat_v1_0 )或二进制数据( NSPropertyListBinaryFormat_v1_0 )具有各自的优缺点(与NSKeyedArchiever几乎相同)。 还有另一种格式– OpenStep( NSPropertyListOpenStepFormat ),该格式现已不推荐使用,仅可用于读取。

在这里 您可以找到有关plist二进制格式结构的一些详细信息)

序列化和反序列化调用中都有一个“选项”参数。 它与可变性有关。 从plist数据反序列化对象时,可以告诉序列化程序是否要使用可变容器-NSPropertyListMutableContainers值。 如果传递NSPropertyListMutableContainersAndLeaves,则串行器将尝试使所有值可变(如果值类具有对应的可变对象)。

将对象序列化为二进制数据时,您无法保存对象的可变性信息,因此options参数毫无意义,并且您始终会传递0

可编码

所有这些用于序列化的API对于ObjC都足够好,但是对Swift来说有一些明显的限制。 我已经提到过NSCoding不能与swift的本机结构和枚举一起使用(尽管开发人员想出了一些解决方法),如果要在swift的类中使用它,则必须使它们在ObjC中可见。 此外,还需要针对Swift的强类型安全性调整所有序列化API。 因此,在标准库中创建一些用于序列化的工具是时间问题。

Swift 4中实现了Codable(结合了Encodable和Decodable协议),使开发人员的生活变得更加轻松。

直到Swift 4都没有内置的用于数据序列化的本地解决方案,大量的第三方库才似乎填补了这一空白。在针对Codable的swift-evolution提案中,作者认为这些解决方案(特别是用于JSON解析)是没有一个既易于使用/实现,又足够安全的类型。

提案中没有明确说明,但显然,通过编译器生成Codable实现的能力(将开发人员的工作量从开发人员转移过来)是我们最终实现的方法的显着优势。

每个协议(可编码/可解码)仅包含一种必需的方法。 好消息是,如果所有对象的属性本身都符合Codable,则编译器将能够生成该对象的实现。 如果不满足条件,或者您需要一些其他自定义,则可以手动编写方法。 对于Encodable,它将如下所示:

  func encode(编码器:编码器)抛出{ 
var container = encoder.container(keyedBy:CodingKeys.self)
尝试container.encode(someObjectProperty,forKey:CodingKeys.someKey)
//处理对象的所有属性
}

基本上,这与编译器为您生成的代码相同。

通过使您的对象符合此协议,您可以立即将它们解码为JSON和plist格式-只需选择一个相应的编码器即可。

编码器是一个对模型对象进行实际序列化的对象。 有两种内置编码器:JSONEncoder和PropertyListEncoder用于相应的格式。 所有系统都是基于协议的,并且有特定的编码器/解码器协议,因此可以为其他格式实现自己的编码器,从而可以与所有Codable模型兼容(例如Mike Ash的示例)。

内置编码器(以及解码器)的用法很简单(只要yourCustomObject符合Codable):

 让jsonData =试试JSONEncoder()。encode(yourCustomObject) 

这样,我们就可以从您的自定义对象到二进制数据进行端到端的序列化。

如果查看一下Swift源代码,您会发现JSONEncoder使用的是Foundation相同的NSJSONSerialization(从swift使用时称为JSONSerialization),与我之前提到的相同。 JSONEncoder将模型对象的Encodable实现用作如何将其所有属性包装到NSObjects中的指令。 然后将它们放入一个容器(实际上是一个NSObjects数组),并将该容器传递给JSONSerialization以便转换为二进制数据。 因此,基本上,JSONEncoder利用Encodable实现将您的自定义对象映射到一个数据结构中,该数据结构可以由良好的旧NSJSONSerialization使用。 因此,Swift安全,优雅地完成了我们开发人员之前的工作:对自定义模型的属性执行某种映射到基于Foundation的容器中,然后使用NSJSONSerialization。

对于PropertyListEncoder也是如此。

(如果您想阅读有关Codable / Encodable / Decodable的复杂用例以及JSONEncoder的所有功能的详细信息,请参阅 这篇 文章)

一些基准和比较

很难公平地比较所有不同方法的速度或结果数据大小,因为它们最初满足的是完全不同的需求。

NSKeyedArchiver(以及之前的NSArchiever)假定处理复杂的对象图。 它考虑了诸如对同一对象的多次引用,对象替换,调用每个对象的encodeWithCoder()方法,对象图等之类的事情。 如果您使用二进制输出,则还需要付出额外的努力才能使输出变小。 因此,毫不奇怪,NSKeyedArchiver花费大量时间序列化自定义对象或对象图。

JSON / Plist序列化程序最初只能与字典或数组以及一些预定义的Foundation类型一起使用。 它使编码器的寿命更加轻松,因此可以花费更少的时间并在磁盘空间上更有效。

谈到输出数据的大小,JSON是这三种格式中最紧凑的格式,而plist则是一种较为冗长的格式(相差2-4倍)。 NSKeyedArchiever及其所有元数据的输出所占用的磁盘空间可能是JSON的10倍以上。 但是NSKeyedArchiever序列化的最终最终用例是数据持久性,因此在这里正确的数据结构比所占用的空间更为重要。

在谈论ObjC时,只能对NSDictionary或NSArray(JSON / Plist序列化程序不适用于自定义类型)进行比较,并且结果或多或少是可预测的:JSON是最紧凑的格式,JSON和plist都非常快高效且NSKeyedArchiver很烂(您可以在此处找到基准之一)

但是在Swift中,我们能够将这3种编码器用于符合Codable的自定义类,因此所有条件都相等。

我自己做了一个琐碎的基准测试,以检查实际情况。 在查看了Swift的结果之后,我决定为自定义对象也包括Obj编码。 所有测试均针对小物体完成。

结果如下:

Swift中的序列化。

  • NSKeyedArchiver是最慢的一个-记住我们添加了处理Codable的功能只是为了与该ObjC API兼容。 Codable固有的JSONEncoder / PropertyListEncoder可以更快地完成工作。
  • Plist序列化比JSON序列化慢。 但是对于二进制plist格式,差异几乎可以忽略不计。 二进制plist被认为是“人类不可读的”,但是基本上,这只是您用来打开文件的应用程序的问题。 除非在文本编辑器中打开JSON,否则也不可读😉
  • 对于plist序列化程序和键控归档程序,序列化为二进制格式比XML花费更多的时间。 不要忘记,存档器将更多的元数据添加到输出XML中,因此其输出文件更大。

ObjC中的序列化:

  • 在这里, 我们不将NSKeyedArchiver与JSON / Plist序列化 程序的自定义对象进行比较。 对于序列化程序,我使用了手动映射(对象属性->字典键),该方法比使用键值容器的常规存档器方法要快得多。 因此,对于JSON / Plist,我们仍然序列化字典而不是对象。
  • 将JSON与plist序列化进行比较,我们发现前者的速度是后者的两倍。
  • 我们可以看到plist序列化程序和键控归档程序的二进制格式和XML格式之间没有显着差异。

比较Swift和ObjC:

  • 我将ObjC和Swift的JSON / Plist序列化彼此相邻放置,以显示Codable实现的编码工作量(每种类型的ObjC和Swift条之间的差异)以及ObjC序列化程序的实际工作速度有多快(这很常见)两种语言)
  • 签出NSKeyedArchiver:Swift在这里也变慢也就不足为奇了,键控存档器完全是一个ObjC API。

NSKeyedArchiver与Codable合作

引入Codable之后,使用来自Swift的NSKeyedArchiver的必要性几乎消失了。 现在,可以将相同的对象存储在磁盘上,您可以使用JSONEncoder或PropertyListEncoder进行序列化。 NSKeyedArchiver不再是序列化自定义对象的主要编码器。 但是,如果要在符合Codable的模型中使用它,可以做到这一点(无需实现NSCoding协议方法)。 Swift团队专门为NSKeyedArchiver引入了两种新方法来使其与Codable一起使用:

 扩展NSKeyedArchiver { 
公共函数encodeCodable(_ codable:Encodable ?, forKey键:字符串){…}
}
 扩展NSKeyedUnarchiver { 
公共功能解码Codable (_类型:T.Type,forKey键:字符串)-> T? {…}
}

为了对根对象进行编码,您可以使用如下编码方法:

 做{ 
让Archiver = NSKeyedArchiver(requireringSecureCoding:false)
archiver.outputFormat = .binary
尝试archiver.encodeEncodable(yourObject,forKey:NSKeyedArchiveRootObjectKey)
archiver.finishEncoding()

//然后您可以使用编码数据
试试archiver.encodedData.write(to:fileUrl)
} {
打印(“无法写入文件:\(错误)”)
}

另一种情况是,您想用JSONEncoder编码根对象。 该对象几乎符合Codable,但是它仅具有不能采用Codable的旧ObjC类的一个属性。 因此,您的选择是手动实现Codable的encode()方法,在该方法中,您可以使用NSKeyedArchiver将此旧属性转换为二进制数据:

  struct Person:可编码{ 
命名:字符串
let workHistory:WorkHistory //无法扩展为采用Codable,但符合NSCoding
 枚举CodingKeys:字符串,CodingKey { 
案例名称
案例工作历史
}
  func encode(编码器:编码器)抛出{ 
var container = encoder.container(keyedBy:CodingKeys.self)
尝试container.encode(name,forKey:.name)
让workHistoryData =尝试NSKeyedArchiver.archivedData(withRootObject:workHistory,requireSecureCoding:false)
尝试container.encode(workHistoryData,forKey:.workHistory)
}
  init(来自解码器:解码器)抛出{ 
让容器=尝试解码器。容器(keyedBy:CodingKeys.self)
名称=尝试container.decode(String.self,forKey:.name)
让workHistoryData =试试container.decode(Data.self,forKey:.workHistory)
workHistory =试试NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(workHistoryData)作为! 工作经历
}
}

其他格式

当然,JSON和Plist不是唯一可用的格式。 因此,还有其他针对不同需求的序列化方法

对于与网络无关的平台数据交换,有一些替代方案,例如Protobuf或MessagePack,它们声称在各种不同指标上都更好(protobuf甚至具有官方的Swift库)。

对于本地数据持久性,有FastCoding。

还有一些其他流行的协议尚未移植到Cocoa平台,例如Facebook的Apache Thrift或Google的FlatBuffers。

就我个人而言,我没有机会在生产中使用它们中的任何一个,因此我只提到它们作为可能的替代品。 您可以找到许多有关格式的信息,处理这些格式的库以及一些使用案例,其中有必要研究这些替代方案。

编码复杂对象图

序列化的最常见用例是磁盘上复杂对象图的持久性。 (例如在NSHipster的这篇文章中,马特(Matt)认为NSCoding / NSKeyedArchiver是Core Data的有效替代方案。)

所有提到的编码器不仅可以轻松处理简单的对象,而且还可以轻松处理带有许多嵌套项目的复杂对象树。 但是,如果您想对复杂的图形进行编码,则这是另一项任务,并且会带来一些额外的复杂性。

谈到“复杂图”,我指的是至少具有以下条件之一的结构:

  • 对一个对象的几种引用
  • 对象之间的循环引用

如果图在其内部具有对同一对象的多个引用,则问题是要考虑对象的唯一性。 编码器必须具有一些已发生对象的表,以免多次对同一个对象进行编码。 如果一个对象多次传递给编码器,则它应仅对引用进行编码。 最终,当存档被解码回并且图形被重建时,应该引用相同的实例,但不能引用多个不同的对象。

如果图形中有相互引用的对象,则对于编码器来说是棘手的。 如果不考虑这种可能的循环,并且盲目地跟随每个对象的每个链接,它将进入无限循环。

复杂结构的另一个复杂之处在于,归档整个图形可能并不总是合适的。 一个很好的例子是视图层次结构。 视图具有指向其他对象的许多链接:模型,子视图,超级视图,格式化程序,目标,手势识别器等。 如果视图对所有这些对象的引用都进行了编码,则整个应用程序将被拉入。但是,某些对象比其他对象更重要。 视图的子视图始终应该被存档,但不一定要被存档。 在这种情况下,超级视图被视为图的多余部分; 一个视图可以不具有其父视图而存在,但不能具有其子视图。 但是,如果还需要在存档中对超级视图进行编码,则视图需要保留对其超级视图的引用。

NSCoder的API及其具体实现NSKeyedArchiver中考虑了所有这些问题。 对象的唯一性是在一种图形编码的上下文中控制的,该图形编码通过调用encodeRootObject:打开。 多个和循环引用也得到了适当的管理:没有无限循环,每个对象只有一个实例。

NSKeyedArchiver还有一系列其他有用的功能,例如缺少键的默认值,键入强制或替换对象的功能。 你可以在这里阅读更多

不幸的是,NSKeyedArchiver的大部分好处仅适用于采用NSCoding协议的对象。 引用控制和循环依赖项解析都不适用于Codable。

此外,可编码模型的行为与JSONEncoder和PropertyListEncoder相同,如果谈论复杂的图,则会遇到相同的问题。 因此,对于Codable当前没有针对此问题的内置解决方案。

Swift团队从一开始就保证此功能在Codable的计划中,但是他们没有时间在Swift 4发行之前及时完成它。 主要困难与Swift初始化过程有关,该过程比ObjC的初始化过程严格得多。 仍在进行中,讨论仍在进行中,希望我们很快能在标准库中看到一些解决方案。

在此之前,您可以使用几个选项在Swift中对复杂的图结构进行编码/解码:

  1. 使用NSKeyedArchiver + NSCoding。 良好的旧ObjC API可在Swift中正常运行。
  2. 在调用代码和实际编码器之间实现一个附加的兼容性层。 创建一个 reference table 以检查编码对象的身份。
  3. 实现自己的编码器/解码器对。 这是最耗时的选项,但是最终通过这种方式,您可以将综合实现用于模型中的实际编码/解码。
  4. 您可以使用一些第三方解决方案。 这个问题很常见,因此已经有一些尝试实现解决方案的尝试。 https://github.com/BigZaphod/Archivable,https://github.com/cherrywoods/swift-meta-serialization

在这里和这里的快速论坛中,有两个非常有益的讨论。

可编码可替代NSCoding(作为结论)

显然,可编码是iOS / macOS平台中序列化的未来。 那么我们是否可以将NSCoding视为传统API,而在考虑Swift时就将其忘却呢?

可编码(使用Coder协议)来自NSCoding(具有NSCoder抽象类),并继承了其通用机制。 我们将编码器传递到模型的encode()方法中,模型将自身转换为许多键值对,然后将其传递给编码器。 但是Codable涵盖的用例数量不同。

在开始时,我们指出,甚至在Codable之前,我们就应该将序列化与存档区分开来,但是我们利用NSCoding的唯一方法是数据持久性。 与NSCoding一起使用的NSKeyedArchiever以某种格式对数据进行序列化,使得几乎不可能在另一个生态系统中对数据进行反序列化(没有NSKeyedUnarchiver和用于编码的数据模型)。 NSCoding与JSON或plist编码/解码无关。 因此,唯一实用的方法是在应用启动之间保留数据。

Codable引入了一种完全不同的方法。 编码实现不仅可以用于将对象序列化为JSON或plist,还可以用于序列化某些自定义格式。 它使序列化与归档解耦,并使它更加独立。 在这方面,多用途可编码比NSCoding具有更大的价值。

NSCoding有其自身的优势。 让我们再提一次。

您可以使用两种语言的NSCoding。 在混合ObjC / Swift项目中,当用ObjC编写旧模型时,是否用Swift编写所有新代码都没关系-您仍然无法基于Codable构建通用编码机制(适用于两种语言)。

第二点是NSCoding在Apple框架中的广泛采用。 所有Foundation值对象以及大多数Application Kit和UIKit用户界面对象均采用NSCoding。 现在采用Codable的类列表不是很多。 因此,为要序列化/存档的类编写扩展可能会花费很多精力(例如,如果要保留应用程序的视图层次结构)。

要序列化复杂的对象图,如果没有其他工具,就不能使用纯Swift和Codable。 但是您可以使用NSCoding轻松实现。

至少在现在,我认为Codable不能完全替代我们代码中的NSCoding。 您可以将其优势用于某些用例,例如JSON编码或简单结构的序列化,以将其存储在磁盘上。 但是,仍有一些用例是NSCoding是一种更好的方法。 我们的工作是了解这两种方法的特殊性和局限性,并在适当的情况下使用它们。