序列化与反序列化

概述

序列化(serialization),是指把对象状态转换成可以存储或传输的形式。反序列化(deserialization)则是它的逆过程。

对象状态典型的如实例对象,存储或传输的形式常以字节序列/二进制数据存在,可以序列化和反序列化的过程可简单用公式表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 对象 ⇌ 反序列化 序列化 二进制数据 {\text{对象} \mathop{ \rightleftharpoons }\limits_{{\text{反序列化}}}^{{\text{序列化}}}\text{二进制数据}} </math>对象反序列化⇌序列化二进制数据

而一些更容易理解的概念,如加密、持久后,则是在这个过程之前或之后的处理过程。

每种序列化协议都存在优缺点,在设计之初有自己独特的应用场景,客户端对序列化协议的选型一般考量:

  • 通用性:技术层面跨平台、跨语言。
  • 健壮性。
  • 可读性。
  • 性能。
  • 兼容性、扩展性。
  • 安全性。

基于以上到因素到考量,iOS 中常用的序列化方案有:

  1. 基于 NSCodingNSSecureCoding 的归档方案。
  2. XML 协议。
  3. JSON 协议方案。
  4. Protocol Buffers 协议方案。

以上方案的划分是基于序列后的数据格式划分的。

一些教科书上会讲到"计算机文件基本上分为两种:二进制文件和纯文本文件。"虽然这种说法有些胡扯,毕竟从物理角度上来说最后存储成二进制数据,只是说纯文本文件是基于字符编码而已,而所谓的排除纯文本文件的"二进制文件"则是自定义编码。尝试从编码角度可以再次对上述方案进行划分:

  • 对象 <-> 二进制数据:归档、Protocol Buffers。
  • 对象 <-> 文本 <-> 二进制数据:XML、JSON。

前三种有 iOS 的原生支持,本文内容仅讨论前三种的实现应用。

原生支持

在 Swift 中,标准库定义了 EncodableDecodableCodable,以及 EncoderDecoder API 来执行编码和解码,参阅 Encoding, Decoding, and Serialization。Foundation 通过 EncodableWithConfigurationDecodableWithConfiguration 协议进行扩展,用于需要额外静态信息进行编码和解码的类型,如 AttributedString

在 Objective-C 中,NSCoding 定义来对象进行编码和解码的协议。在向自定义类型添加序列化时,还应遵循 NSSecureCoding 协议,该协议增加了对作为解码过程的一部分实例化任意对象引入的安全漏洞的保护。

许多系统框架使用这些类型。在使用外部系统(例如 URL 端点)时,使用 JSON 和 XML API 将应用程序的类型序列化为标准格式。

NSCoding 主要的限制是需要依赖 Foundation,且需要继承于 NSObject,而 Codable 就不需要,但 Codable 仅支持 Swift,Objective-C 上不能使用,另外 Codable 功能也更灵活,能清松吧模型转换成 JSON 或 XML。

基本功能

  • 基本转换:
    • json <-> model
    • plist <-> model
    • dictionary <-> model
  • 自定义 key mapping
  • 值处理

方案:

  • NSCoding
  • Serialization
  • Codable
  • YYModel
  • JSONModel
  • SwiftyJSON
  • ObjectMapper
  • BDModel
  • Mantle

内容:

  • 原理、定义、支持功能
  • 使用场景
  • 限制
  • 基本使用

NSCoding + NSCoder

NSCoding 协议定义了编码和解码的方法。为归档(把对象和其他结构存储到磁盘中)和分发(把对象复制到不同的地址空间)提供基础。根据面相对象设计原则,由遵循 NSCoding 协议的对象进行编码和解码。

yaml 复制代码
# 解码。从 coder 数据构建自身。
init?(coder: NSCoder)

# 编码。将自身编码到 coder。
func encode(with coder: NSCoder)

NSCoding 协议用于自定义的模型,声明要编解码那些 key,NSCoder 具体子类负责将具体的模型对象序列化为 Data,或将 Data 反序列化为模型对象。

在 iOS 12.0+ 中,苹果推荐使用 NSSecureCoding 协议,这个协议在 NSCoding 的基础上,还需要实现一个静态的属性:

swift 复制代码
static var supportsSecureCoding: Bool { true }

NSSecureCoding 的安全主要体现在增加了类型安全的校验,确保数据仅能被源编码类型解码。

名词解释:

  • 归档(archiving):把对象和其他结构存储到磁盘中。
  • 分发(distribution):把对象复制到不同的地址空间。

编解码器(coder。编解码器在音视频领域是 codec)可分为编码器(encoder,只能解码)和解码器(decoder,只能编码)。

  • 归档(archive):可理解为编码、序列化过程。
  • 解档(unarchive):可理解为解码、反序列化过程。

NSCoder 抽象类,作为支持归档和分发对象的基础,声明了具体子类在内存和其他格式之间传输对象和值的接口,即定义了编解码器的接口。

NSCoder 对对象、标量、C 数组、结构体、字符串以及指向这些类型的指针进行操作。它不处理实现因平台而异的类型,例如联合体、无类型指针(void *)、函数指针和指针长链。编码器将对象类型信息与数据一起存储,因此从字节流解码的对象通常与最初编码到流中的对象属于同一类型。但是,对象可以在编码时更改其类型。

具体用于实例化子类为:

  • NSArchiver:编码器。弃用,macOS 10.0--10.13。
  • NSUnarchiver:解码器。弃用,macOS 10.0--10.13。
  • NSKeyedArchiver:键控编码器。iOS 2.0+,macOS 10.2+。
  • NSKeyedUnarchiver:键控解码器。iOS 2.0+,macOS 10.2+。
  • NSPortCoder:分布式对象系统中传输对象(代理)的编解码器。弃用,macOS 10.0--10.13。

当然也支持自己实现一个 NSCoder 子类,具体可参阅 Archives and Serializations Programming Guide 中的 Subclassing NSCoder

上述的具体编解码器编码的目标产物和解码的来源数据都是表示二进制数据的 NSData/Data。该二进制数据与架构无关,可直接进行归档和分发。

给定用于编码值的 key 必须在当前编码对象的范围内保持唯一,即一个类型中,每个属性的编码 key 是唯一的。

键控存档与非键控存档的不同之处在于,编码到存档中的所有对象和值都有名称或键。解码时必须使用编码时使用的 key。解码键控存档时,解码器按 key 请求值,意味着可以不按顺序编解码,同时也为向前和向后兼容性提供了更好的支持。这也是为什么 Apple 要废弃掉 NSArchiverNSUnarchiver 的原因。

为更好地支持归档类型,在 macOS 10.2 及更高版本中,首选键控归档(keyed archiving)。

无脑选择:NSSecureCoding + 键控归档/解档。

使用场景

Model: NSObject <-> Data

优势:

  • 线程安全。
  • 直接编码成 Data,有一定安全性,同时也难以从 Data 产物中排查问题。
  • NSSecureCoding 支持类型检查,更加安全。

限制:

  • 遵循协议的类需是 NSObject 的子类。
  • 不支持自定义 key。
  • 只能序列化为 Data,并从 Data 反序列化为模型对象。

API 分析

NSCoder

用于 NSCoding 中编解码的 NSCoder API,都存在一一对应的关系:

yaml 复制代码
# 还有一系列不同类型的重载函数,函数签名只有 value 的类型不一样
func encode(
    _ value: Bool,
    forKey key: String
)
# 还有一系列具体类型的 decodeXxx(forKey:) 方法用于解码具体的类型
func decodeBool(forKey key: String) -> Bool
# 对于 Swift 的类型,可直接使用该方法
func decodeObject(forKey key: String) -> Any?

func decodeObject<DecodedObjectType>(
    of cls: DecodedObjectType.Type,
    forKey key: String
) -> DecodedObjectType? where DecodedObjectType : NSObject, DecodedObjectType : NSCoding
@nonobjc func decodeObject(
    of classes: [AnyClass]?,
    forKey key: String
) -> Any?

@nonobjc func decodeTopLevelObject(forKey key: String) throws -> Any?
@nonobjc func decodeTopLevelObject(
    of classes: [AnyClass]?,
    forKey key: String
) throws -> Any?
func decodeTopLevelObject<DecodedObjectType>(
    of cls: DecodedObjectType.Type,
    forKey key: String
) throws -> DecodedObjectType? where DecodedObjectType : NSObject, DecodedObjectType : NSCoding

# 字节 buffer
func encodeBytes(
    _ bytes: UnsafePointer<UInt8>?,
    length: Int,
    forKey key: String
)
func decodeBytes(
    forKey key: String,
    returnedLength lengthp: UnsafeMutablePointer<Int>?
) -> UnsafePointer<UInt8>?

# Objective-C 数组,需指定元素类型。
func encodeArray(
    ofObjCType type: UnsafePointer<CChar>,
    count: Int,
    at array: UnsafeRawPointer
)
func decodeArray(
    ofObjCType itemType: UnsafePointer<CChar>,
    count: Int,
    at array: UnsafeMutableRawPointer
)

decodeObject(of:forKey:) 调用需要求该类型遵循 NSSecureCoding,否则会失败(failWithError(_:))。

decodeObject(of:forKey:) 重载支持了传入一组类型,使用规则类似,区别只是可以传入多个候选类型。

相对比 decodeObject(forKey:) 则适用于类型只是遵循了 NSCoding,或 NSSecureCodingrequiresSecureCodingfalse 的情况。

NSKeyedArchiver/NSKeyedUnarchiver

NSKeyedArchiverNSKeyedUnarchiver 我们常用其类方法一步到位完成归档和解档:

yaml 复制代码
# NSKeyedArchiver 归档。iOS 11.0+。
class func archivedData(
    withRootObject object: Any,
    requiringSecureCoding requiresSecureCoding: Bool
) throws -> Data
# NSKeyedArchiver 归档。iOS 2.0--12.0。
class func archivedData(withRootObject rootObject: Any) -> Data

# iOS 2.0--12.0。
class func unarchiveObject(with data: Data) -> Any?
class func unarchiveObject(withFile path: String) -> Any?
# NSKeyedUnarchiver 解档。iOS 11.0+。
@nonobjc static func unarchivedObject<DecodedObjectType>(
    ofClass cls: DecodedObjectType.Type,
    from data: Data
) throws -> DecodedObjectType? where DecodedObjectType : NSObject, DecodedObjectType : NSCoding
class func unarchivedObject(
    ofClasses classes: Set<AnyHashable>,
    from data: Data
) throws -> Any
@nonobjc static func unarchivedObject(
    ofClasses classes: [AnyClass],
    from data: Data
) throws -> Any?

NSKeyedUnarchiverunarchivedObject(ofClass:from:) 要求 Data 是由 NSSecureCoding 的类型实例编码而成的,否则会失败。

小结

可见,上述 API 中需要传入类型参数的都是面相遵循 NSSecureCoding 的对象,否则直接在运行时抛出错误。

实践

以下示例中展示了 NSCodingNSSecureCodingNSKeyedArchiverNSKeyedUnarchiver 的详细使用。

yaml 复制代码
class Song: NSObject {
    init(artist: String, title: String, assetURL: URL, discNumber: Int) {
        self.artist = artist
        self.title = title
        self.assetURL = assetURL
        self.discNumber = discNumber
    }

    var artist: String
    var title: String
    var assetURL: URL
    var discNumber: Int
}

extension Song {
    enum CodingKeys: String, CodingKey {
        case artist, title, assetURL, discNumber
    }
}

extension NSCoder {
    func encode(_ value: Any, key: Song.CodingKeys) {
        encode(value, forKey: key.rawValue)
    }

    func decode<T>(of: T.Type? = nil, key: Song.CodingKeys) -> T? {
        decodeObject(forKey: key.rawValue) as? T
    }

    func decodeSecurely<T: NSCoding & NSObject>(of: T.Type? = nil, key: Song.CodingKeys) -> T? {
        decodeObject(of: T.self, forKey: key.rawValue)
    }
}

// MARK: NSCoding

class CodingSong: Song, NSCoding {
    func encode(with coder: NSCoder) {
        // 使用 @objc 可使用 #keyPath(artist) 生成字符串
        // @objc var artist: String
        // coder.encode(artist, forKey: #keyPath(artist))

        coder.encode(artist, key: .artist)
        coder.encode(title, key: .title)
        coder.encode(assetURL, key: .assetURL)
        coder.encode(discNumber, key: .discNumber)
    }

    required convenience init?(coder: NSCoder) {
        let artist: String = coder.decode(key: .artist)!
        let title: String = coder.decode(key: .title)!
        let assetURL: URL = coder.decode(key: .assetURL)!
        let discNumber: Int = coder.decode(key: .discNumber)!
        self.init(artist: artist, title: title, assetURL: assetURL, discNumber: discNumber)
    }
}

// MARK: NSSecureCoding

class SecureCodingSong: CodingSong, NSSecureCoding {
    required convenience init?(coder: NSCoder) {
        let artist: NSString = coder.decodeSecurely(key: .artist)!
        let title: NSString = coder.decodeSecurely(key: .title)!
        let assetURL: NSURL = coder.decodeSecurely(key: .assetURL)!
        let discNumber: NSNumber = coder.decodeSecurely(key: .discNumber)!
        self.init(artist: artist as String, title: title as String, assetURL: assetURL as URL, discNumber: discNumber.intValue)
    }

    static var supportsSecureCoding: Bool { true }
}

class CommonCodingSong: Song, NSSecureCoding {
    func encode(with coder: NSCoder) {
        coder.encode(artist, key: .artist)
        coder.encode(title, key: .title)
        coder.encode(assetURL, key: .assetURL)
        coder.encode(discNumber, key: .discNumber)
    }

    required convenience init?(coder: NSCoder) {
        if Self.supportsSecureCoding {
            let artist: NSString = coder.decodeSecurely(key: .artist)!
            let title: NSString = coder.decodeSecurely(key: .title)!
            let assetURL: NSURL = coder.decodeSecurely(key: .assetURL)!
            let discNumber: NSNumber = coder.decodeSecurely(key: .discNumber)!
            self.init(artist: artist as String, title: title as String, assetURL: assetURL as URL, discNumber: discNumber.intValue)
        } else {
            let artist: String = coder.decode(key: .artist)!
            let title: String = coder.decode(key: .title)!
            let assetURL: URL = coder.decode(key: .assetURL)!
            let discNumber: Int = coder.decode(key: .discNumber)!
            self.init(artist: artist, title: title, assetURL: assetURL, discNumber: discNumber)
        }
    }

    static var supportsSecureCoding: Bool { true }
}

func testCoding() {
    print("testing NSCoding")
    typealias Song = CodingSong
    let song = Song(artist: "万能青年旅店", title: "冀西南林路行", assetURL: URL(fileURLWithPath: "./song.mp3"), discNumber: 666)
    do {
        // 归档
        let data = try NSKeyedArchiver.archivedData(withRootObject: song, requiringSecureCoding: false)
        print("ecoded data: \(data)")

        // 解档
        let decodedSong = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
        dump(decodedSong, name: "DecodedSong")
    } catch {
        print(error)
    }
}

func testSecureCoding() {
    print("testing NSSecureCoding")
    typealias Song = SecureCodingSong
    let song = Song(artist: "万能青年旅店", title: "冀西南林路行", assetURL: URL(fileURLWithPath: "./song.mp3"), discNumber: 666)
    do {
        // 归档
        let data = try NSKeyedArchiver.archivedData(withRootObject: song, requiringSecureCoding: true)
        print("ecoded data: \(data)")

        // 解档
        // 这里要求都需要 SecureCoding
        let decodedSong = try NSKeyedUnarchiver.unarchivedObject(ofClass: Song.self, from: data)
        dump(decodedSong, name: "DecodedSong")
    } catch {
        print(error)
    }
}

func testCodec() {
    testCoding()
    testSecureCoding()
}

实用技巧:

  • 因为 NSCoding 实现中需要按 key 编码和解码数据,这要求编码和解码使用的 key 需要保持一致。这里通过定义了一组 CodingKeys 枚举来确保 key 的复用。
  • 除了上述 CodingKeys 枚举的方式复用 key,也可以使用 @objc 修饰属性(所在的类需为 NSObject 子类),然后使用 #keyPath(属性) 的方式生成字符串。
  • 这里为了缩写 API 和简化 CodingKeys 枚举的使用,编写了几个 NSCoder 扩展方法,让代码逻辑更加简练清晰。

注意:

  • 上述的 CodingSong 类实现了 NSCodingSecureCodingSong 类实现了 NSSecureCoding,也可以像 CommonCodingSong 那样直接支持两种协议。但基于安全性考虑,还是建议大家仅实现 NSSecureCoding
  • 如在 CommonCodingSong 中,可以通过自身的 supportsSecureCoding 属性控制自身是实现 NSCoding 还是 NSSecureCoding
  • NSCodingNSSecureCoding 在编码的 API 都是一致的,只有在解码的时候才需要调用不同的 API。

也可以把 object 关联到 key 中,即手动完成 encode/decode:

swift 复制代码
let person = Person(firstName: "Bq", lastName: "Lin", age: 18)

let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("person_bq.bin")

let key = "bq"
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
archiver.encode(person, forKey: key)
archiver.finishEncoding()
try! archiver.encodedData.write(to: url)

let data = try! Data(contentsOf: url)
let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: data)
let obj = unarchiver.decodeObject(of: Person.self, forKey: key)!
dump(obj)

更多资料

Serialization

JSONSerializationPropertyListSerialization。故名思义,这两个类是 Foundation 原生提供的用于 JSON 和 plist 序列化和反序列化的实现。即使后面讲述的 Codable 是独立于系统的 Swift 实现,但在 Apple 的操作系统中,还是会使用这两个类进行 JSON 和 plist 的转换。

JSONSerialization

使用 JSONSerialization 来实现 object <-> JSON Data。object 限制为:

可以调用 isValidJSONObject(_:) 进行校验或尝试进行转换。

API 接口:

yaml 复制代码
# 校验,否则直接抛出异常,使用 try 无法捕获。
class func isValidJSONObject(_ obj: Any) -> Bool

# 序列化对象
class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any
class func jsonObject(with stream: InputStream, options opt: JSONSerialization.ReadingOptions = []) throws -> Any

# 反序列化 Data
class func data(withJSONObject obj: Any, options opt: JSONSerialization.WritingOptions = []) throws -> Data
class func writeJSONObject(_ obj: Any, to stream: OutputStream, options opt: JSONSerialization.WritingOptions = [], error: NSErrorPointer) -> Int

注意:序列化前,需对输入的对象调用 isValidJSONObject(_:) 进行校验,对不符合上述要求的数据将返回 false,否则 JSONSerialization 直接抛出异常,使用 try 都无法捕获。

由于 JSON 格式本来没有类型校验,所以在序列化/反序列化过程中对象都是 Any 类型,使用过程中还需要用户再进行类型转换。

PropertyListSerialization

使用 PropertyListSerialization 来实现 array/dictionary <-> plist data。

API 接口:

yaml 复制代码
# 序列化 plist
class func data(fromPropertyList plist: Any, format: PropertyListSerialization.PropertyListFormat, options opt: PropertyListSerialization.WriteOptions) throws -> Data
class func writePropertyList(_ plist: Any, to stream: OutputStream, format: PropertyListSerialization.PropertyListFormat, options opt: PropertyListSerialization.WriteOptions, error: NSErrorPointer) -> Int

# 反序列化 plist
class func propertyList(from data: Data, options opt: PropertyListSerialization.ReadOptions = [], format: UnsafeMutablePointer<PropertyListSerialization.PropertyListFormat>?) throws -> Any
class func propertyList(with stream: InputStream, options opt: PropertyListSerialization.ReadOptions = [], format: UnsafeMutablePointer<PropertyListSerialization.PropertyListFormat>?) throws -> Any

# 校验
class func propertyList(_ plist: Any, isValidFor format: PropertyListSerialization.PropertyListFormat) -> Bool

使用场景

NSArray/NSDictionary <-> JSON data、plsit data。

优势:

  • 线程安全。

限制:

  • 不能直接转换具体的类,只能是些基本值,对于具体的类,需转换成数组或字典。
  • 虽然是系统自带,但性能不一定是最佳,当遇到性能瓶颈,可考虑替换这两个类的实现。

Codable + Encoder/Decoder

Swift only,与上面的 NSCoding 类型不同,Coable 可协议用于所有的自定义的类型。可以把自身类型转换为外部表示的类型(如 JSON、plist)。遵循 Encoder/Decoder 协议的类负责完成具体的编码和解码/序列化和反序列具体逻辑。目前 Foundation 提供的编解码器有:

  • PropertyListEncoderPropertyListDecoder
  • JSONEncoderJSONDecoder

当然其内部还是使用 JSONSerializationPropertyListSerialization 来做具体的转换。

大多数内置的 Swift 类型都默认支持 Codable。自定义类型(class、struct、enum)只要其所有属性都遵循 Codable 类型,声明为 Codable,不用编写任何额外代码。enum 特殊点,只要有声明了原始类型,才能声明为 Codable

能声明为 Codable 类型,就能够自动序列化为以属性名称为 key 的 JSON、plist 等二进制数据,或从中反序列为具体的 Codable 类型。

自定义能力:

  • 属性类型声明为 Optional,可以用于表示可能不存在的属性。
  • 定义嵌套类型 enum CodingKeys: String, CodingKey 可以自定义属性名到编码 key 的映射。case 名为属性名,rawValue 为对应的编码 key。
  • 自定义 Codable 的两个方法可以自定义编解码的所有细节。

API 接口

swift 复制代码
typealias Codable = Decodable & Encodable

public protocol Encodable {
    func encode(to encoder: Encoder) throws
}

public protocol Decodable {
    init(from decoder: Decoder) throws
}

public protocol Encoder {
    var codingPath: [CodingKey] { get }
    var userInfo: [CodingUserInfoKey : Any] { get }
    func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
    func unkeyedContainer() -> UnkeyedEncodingContainer
    func singleValueContainer() -> SingleValueEncodingContainer
}

public protocol Decoder {
    var codingPath: [CodingKey] { get }
    var userInfo: [CodingUserInfoKey : Any] { get }
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
    func unkeyedContainer() throws -> UnkeyedDecodingContainer
    func singleValueContainer() throws -> SingleValueDecodingContainer
}

使用场景

Model: Any/[Model] <-> JSON/plist/... Data

优势:

  • 一步到位从 Model 到 JSON/plist/... Data。
  • 灵活,各个阶段支持自定义。

限制:

  • 类继承时,需要自定义 Codable 的两个方法,不然只会编解码当前类定义的属性,不会继承父类的属性。
  • 虽然支持自定义序列化过程,但还是不够易用。

使用

基本使用:

Swift 复制代码
// 不需要写 encode(with:) 和 init(coder:) 的协议方法
// 因为协议扩展 extension Codable 中提供了默认实现
class Person: NSObject, Codable {
    var firstName: String
    var lastName: String
    var age: Int

    override var descirption: String {
        return "\(self.firstName) \(self.lastName) \(age)"
    }

    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }
}

// 编解码
let docsurl = try FileManager.default.url(for: .docmentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let filePath = docsurl.appendingPathComponent("person.txt")

let person = Perosn(firstName: "yuuki", lastName: "lili", age: 20)
// 写入
let encodedPerson = try PropertyListEncoder().encode(person)
encodedPerson.write(to: filePath, options: .atomic)
// 读取
let contents = try Data(contentOf: filePath)
let decodedPerson = try PropertyListDecoder().decode(Person.self, from: contents)
// "yuuki lili 20"
print(decodedPerson)

使用 Codable 存储 NSCoding 数据。因为 Cocoa 中很多类只是遵循了 NSCoding 协议,而不是 Codable 协议,所以会有两者一起使用的场景。使用策略是通过 Data 来作为中间的桥梁。

Swift 复制代码
struct Person {
    var name: String
    var favoriteColor: UIColor // NSCoding
}

extension Person: Codable {
    // 因为我们需要显示的声明编码和解码的内容,因此需要在这里写出 CodingKeys
    enum CodingKeys: String, CodingKey {
        case name
        case favoriteColor
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // 字符串类型,直接解码
        name = try container.decode(String.self, forKey: .name)

        let colorData = try container.decode(Data.self, forKey: .favoriteColor)
        // NSCoding 方式
        favoriteColor = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(colorData) as? UIColor ?? UIColor.black
    }

    func encode(to encoder: Encoder) throws {
        var container = try encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        // NSCoding 方式
        let colorData = try NSKeyedArchiver.archivedData(withRootObject: favoriteColor, requiringSecureCoding: false)
        try container.encode(colorData, forKey: .favoriteColor)
    }
}

let taylor = Person(name: "Taylor Swift", favoriteColor: .blue)
let encoder = JSONEncoder()
let decoder = JSONDecoder()

do {
    // 编码
    let encoded = try encoder.encode(taylor)
    // 解码
    let person = try decoder.decode(Person.self, from: encoded)
    print(person.favoriteColor, person.name)
} catch {}

自定义集合类型的编解码:

Swift 复制代码
 struct Student: Codable {
    let scores: [Int] = [66, 77, 88]

    enum CodingKeys: String, CodingKey {
        case scores
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 创建一个对数组处理用的容器 (UnkeyedEncdingContainer)
        var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .scores)
        // 处理后序列化
        try scores.forEach {
            try unkeyedContainer.encode("\($0) 分")
        }
    }
}

let res = """
{
    "gross_score": 120,
    "scores": [
        0.65,
        0.75,
        0.85
    ]
}
"""

struct Student: Codable {
    let grossScore: Int
    let scores: [Float]

    enum CodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
    }

    init(grossScore: Int, scores: [Float]) {
        self.grossScore = grossScore
        self.scores = scores
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let grossScore = try container.decode(Int.self, forKey: .grossScore)

        var scores = [Float]()
        // 处理数组时所使用的容器 (UnkeyedDecodingContainer)
        var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .scores)
        // isAtEnd:A Boolean value indicating whether there are no more elements left to be decoded in the container.
        while !unkeyedContainer.isAtEnd {
            let proportion = try unkeyedContainer.decode(Float.self)
            let score = proportion * Float(grossScore)
            scores.append(score)
        }
        self.init(grossScore: grossScore, scores: scores)
    }
}

处理派生类对象时,需要自定义实现 Codable 方法:

Swift 复制代码
class Ponit2D: Codable {
    var x = 0.0
    var y = 0.0
    // 标记为 private
    private enum CodingKeys: String, CodingKey {
        case x
        case y
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
    }
}

// 1
class Ponit3D: Ponit2D {
    var z = 0.0
    // 标记为 private
    private enum CodingKeys: String, CodingKey {
        case z
    }

    override func encode(to encoder: Encoder) throws {
        //调用父类的 encode 方法将父类的属性 encode
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(z, forKey: .z)
    }
}

//{
//  "x" : 0,
//  "y" : 0,
//  "z" : 0
//}

// 2
class Ponit3D: Ponit2D {
    var z = 0.0

    private enum CodingKeys: String, CodingKey {
        case z
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 创建一个提供给父类 encode 的容器来区分父类属性和派生类属性
        try super.encode(to: container.superEncoder())
        try container.encode(z, forKey: .z)
    }
}

//{
//    "super" : {
//        "x" : 0,
//        "y" : 0
//    },
//    "z" : 0
//}

// 3
class Ponit3D: Ponit2D {
    var z = 0.0

    private enum CodingKeys: String, CodingKey {
        case z
        case point2D //用于父类属性容器的 key 名
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 创建一个提供给父类 encode 的容器来区分父类属性和派生类属性,并将 key 设为 point2D
        try super.encode(to: container.superEncoder(forKey: .Point2D))
        try container.encode(z, forKey: .z)
    }
}

//{
//    "point2D" : {
//        "x" : 0,
//        "y" : 0
//    },
//    "z" : 0
//}

兼容多个版本 API 的模型,使用 userInfo 存储选项:

JSON 复制代码
// version1
{
    "time": "Nov-14-2017 17:25:55 GMT+8"
}

// version2
{
    "time": "2017-11-14 17:27:35 +0800"
}

在 encoder.userInfo 中存储 CodingUserInfoKey 类型的自定义 key 来选择格式。

Swift 复制代码
struct VersionController {
    enum Version {
        case v1
        case v2
    }

    let apiVersion: Version
    var formatter: DateFormatter {
        let formatter = DateFormatter()
        switch apiVersion {
        case .v1:
            formatter.dateFormat = "MMM-dd-yyyy HH:mm:ss zzz"
            break
        case .v2:
            formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
            break
        }
        return formatter
    }
    static let infoKey = CodingUserInfoKey(rawValue: "dateFormatter")!
}

func encode<T>(of model: T, optional: VersionController? = nil) throws where T: Codable {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    if let optional = optional {
        // 通过 userInfo 存储版本信息
        encoder.userInfo[VersionController.infoKey] = optional
    }
    let encodedData = try encoder.encode(model)
    print(String(data: encodedData, encoding: .utf8)!)
}

struct SomeThing: Codable {
    let time: Date

    enum CodingKeys: String, CodingKey {
        case time
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 通过 userInfo 读取版本信息
        if let versionC = encoder.userInfo[VersionController.infoKey] as? VersionController {
            let dateString = versionC.formatter.string(from: time)
            try container.encode(dateString, forKey: .time)
        } else {
            fatalError()
        }
    }
}

let s = SomeThing(time: Date())

let verC1 = VersionController(apiVersion: .v1)
try! encode(of: s, optional: verC1)
//{
//    "time" : "Nov-14-2017 20:01:55 GMT+8"
//}
let verC2 = VersionController(apiVersion: .v2)
try! encode(of: s, optional: verC2)
//{
//    "time" : "2017-11-14 20:03:47 +0800"
//}

处理 key 不确定的模型。

有一种很特殊的情况就是我们得到这样一个 json 数据:

Swift 复制代码
let res = """
{
    "1" : {
        "name" : "ZhangSan"
    },
    "2" : {
        "name" : "LiSi"
    },
    "3" : {
        "name" : "WangWu"
    }
}
"""

这时 struct 类型的 Codingkeys 相当于一个 key 为 string,且 key 名不固定的 CodingKey

Swift 复制代码
struct Student: Codable {
    let id: Int
    let name: String
}

struct StudentList: Codable {
    var students: [Student] = []

    init(students: Student ... ) {
        self.students = students
    }

    struct Codingkeys: CodingKey {
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }

        var stringValue: String //json 中的 key
        // 根据 key 来创建 Codingkeys,来读取 key 中的值
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        // 相当于 enum 中的 case
        // 其实就是读取 key 是 name 所应对的值
        static let name = Codingkeys(stringValue: "name")!
    }

    init(from decoder: Decoder) throws {
        // 指定映射规则
        let container = try decoder.container(keyedBy: Codingkeys.self)
        var students: [Student] = []
        for key in container.allKeys { //key 的类型就是映射规则的类型 (Codingkeys)
            if let id = Int(key.stringValue) { // 首先读取 key 本身的内容
                // 创建内嵌的 keyedContainer 读取 key 对应的字典,映射规则同样是 Codingkeys
                let keyedContainer = try container.nestedContainer(keyedBy: Codingkeys.self, forKey: key)
                let name = try keyedContainer.decode(String.self, forKey: .name)
                let stu = Student(id: id, name: name)
                students.append(stu)
            }
        }
        self.students = students
    }

    func encode(to encoder: Encoder) throws {
        // 指定映射规则
        var container = encoder.container(keyedBy: Codingkeys.self)
        try students.forEach { stu in
            // 用 Student 的 id 作为 key,然后该 key 对应的值是一个字典,所以我们创建一个处理字典的子容器
            var keyedContainer = container.nestedContainer(keyedBy: Codingkeys.self, forKey: Codingkeys(stringValue: "\(stu.id)")!)
            try keyedContainer.encode(stu.name, forKey: .name)
        }
    }
}

let stuList2 = try! decode(of: res, type: StudentList.self)
dump(stuList2)
//▿ __lldb_expr_752.StudentList
//  ▿ students: 3 elements
//    ▿ __lldb_expr_752.Student
//      - id: 2
//      - name: "LiSi"
//    ▿ __lldb_expr_752.Student
//      - id: 1
//      - name: "ZhangSan"
//    ▿ __lldb_expr_752.Student
//      - id: 3
//      - name: "WangWu"

let stu1 = Student(id: 1, name: "ZhangSan")
let stu2 = Student(id: 2, name: "LiSi")
let stu3 = Student(id: 3, name: "WangWu")
let stuList1 = StudentList(students: stu1, stu2, stu3)
try! encode(of: stuList1)
//{
//    "1" : {
//        "name" : "ZhangSan"
//    },
//    "2" : {
//        "name" : "LiSi"
//    },
//    "3" : {
//        "name" : "WangWu"
//    }
//}

错误信息:可以通过标准库的编解码 error 获取错误信息。

Swift 复制代码
public enum DecodingError : Error {
    // 在出现错误时通过 context 来获取错误的详细信息
    public struct Context {
        public let codingPath: [CodingKey]
        // 错误信息中的具体错误描述
        public let debugDescription: String
        public let underlyingError: Error?
        public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = default)
    }
    /// 下面是错误的类型
    // JSON 值和 model 类型不匹配
    case typeMismatch(Any.Type, DecodingError.Context)
    // 不存在的值
    case valueNotFound(Any.Type, DecodingError.Context)
    // 不存在的 key
    case keyNotFound(CodingKey, DecodingError.Context)
    // 不合法的 JSON 格式
    case dataCorrupted(DecodingError.Context)
}
public enum EncodingError : Error {
    // 在出现错误时通过 context 来获取错误的详细信息
    public struct Context {
        public let codingPath: [CodingKey]
        // 错误信息中的具体错误描述
        public let debugDescription: String
        public let underlyingError: Error?
        public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = default)
    }
    // 属性的值与类型不合符
    case invalidValue(Any, EncodingError.Context)
}

那么,有没有更灵活易用的方案呢?我们先来着眼看看一些成熟的 Objective-C 方案吧。

由于业务中更常使用 JSON 作为与后端的数据交换,所以下面的方案进针对 JSON 进行序列化/反序列化。

Objective-C JSON <-> Model 方案

这里仅介绍 YYModel 和 JSONModel 的特性和使用,其他方案都大同小异。这里列出些常用的方案介绍:

YYModel

Objective-C 实现。高性能 iOS/OSX 模型转换框架。使用 Objective-C 的 Runtime 对类对象属性进行自动赋值,并提供许多实用的扩展方法。

耗时对比:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> M a n u a l l y < Y Y M o d e l < M J E x t e n s i o n < J S O N M o d e l < M a n t l e Manually < YYModel < MJExtension < JSONModel < Mantle </math>Manually<YYModel<MJExtension<JSONModel<Mantle

使用场景

  • Model: NSObject <-> Dictionary/Array
  • Model: NSObject/[Model] <-> JSON
  • 实现了 NSCodingNSCopying-hash-isEqual:
  • 使用字典配置属性。

优势:

  • 使用 Runtime 动态根据头文件定义的属性完成序列化、反序列化的过程。
  • 高性能:模型转换性能接近手写解析代码。
  • 自动类型转换:对象类型可以自动转换,详情见下方表格。
  • 类型安全:转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
  • 无侵入性:模型无需继承自其他基类。

当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。

JSON/Dictionary Model
NSString NSNumber,NSURL,SEL,Class
NSNumber NSString
NSString/NSNumber 基础类型 (BOOL,int,float,NSUInteger,UInt64,...) NaN 和 Inf 会被忽略
NSString NSDate 以下列格式解析:yyyy-MM-dd yyyy-MM-dd HH:mm:ss yyyy-MM-dd'T'HH:mm:ss yyyy-MM-dd'T'HH:mm:ssZ EEE MMM dd HH:mm:ss Z yyyy
NSDate NSString 格式化为 ISO8601: "YYYY-MM-dd'T'HH:mm:ssZ"
NSValue struct (CGRect,CGSize,...)
NSNull nil,0
"no","false",... @(NO),0
"yes","true",... @(YES),1

自定义能力:

  • 自定义属性名到 JSON key 的映射。
  • 自定义容器元素类型。
  • 自定义属性处理黑名单和白名单。
  • 自定义数据校验。
  • 自定义数据转换。

限制:

Objective-C only。或者在 Swift 中声明一个 Objective-C 的类:

  • 继承于 NSObject;
  • 属性需为变量;
  • 属性应有 @objc 修饰。

实践

ibireme/YYModel: High performance model framework for iOS/OSX.

自动实现 NSObject、NSCoding、NSCopying:

objective-c 复制代码
@interface YYShadow :NSObject <NSCoding, NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGSize size;
@end

@implementation YYShadow
// 直接添加以下代码即可自动完成
- (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; }
- (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; }
- (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; }
- (NSUInteger)hash { return [self yy_modelHash]; }
- (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; }
- (NSString *)description { return [self yy_modelDescription]; }
@end

Model <-> Dictionary/JSON

objective-c 复制代码
// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];

// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];

自定义属性名与 key 映射:

objective-c 复制代码
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"name" : @"n",
             @"page" : @"p",
             @"desc" : @"ext.desc",
             @"bookID" : @[@"id",@"ID",@"book_id"]};
}

容器类属性:

objective-c 复制代码
@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end

@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
    return @{@"shadows" : [Shadow class],
             @"borders" : Border.class,
             @"attachments" : @"Attachment" };
}
@end

黑名单与白名单:

objective-c 复制代码
@interface User
@property NSString *name;
@property NSUInteger age;
@end

@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
    return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
    return @[@"name"];
}
@end

数据校验与自定义转换:

objective-c 复制代码
// JSON:
{
  "name":"Harry",
  "timestamp" : 1445534567
}

// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end

@implementation User
// 当 JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
    NSNumber *timestamp = dic[@"timestamp"];
    if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
    _createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
    return YES;
}

// 当 Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
    if (!_createdAt) return NO;
    dic[@"timestamp"] = @(n.timeIntervalSince1970);
    return YES;
}
@end

JSONModel

使用场景

  • Model: NSObject <-> Dictionary/Array
  • Model: NSObject/[Model] <-> JSON

自定义:

  • 自定义全局属性名与 key 映射。
  • 自定义属性名与 key 的映射,支持跨层级的映射。
  • 自动驼峰命名转换。
  • 可选类型属性。
  • 忽略属性/黑名单。
  • 自定义数据转换。
  • 自定义 getters/setters。
  • 自定义 JSON 校验。
  • 自带 HTTP 请求接口。

限制:

  • 模型需要继承自 JSONModel。
  • 同样也需要 Objective-C 风格的 Model 定义。

实践

基本使用:

objective-c 复制代码
// 类型定义
@interface CountryModel : JSONModel
@property (nonatomic) NSInteger id;
@property (nonatomic) NSString *country;
@property (nonatomic) NSString *dialCode;
@property (nonatomic) BOOL isInEurope;
@end

// JSON -> Mdoel,需要使用初始化方法。
NSError *error;
CountryModel *country = [[CountryModel alloc] initWithString:myJson error:&error];

ProductModel *pm = [ProductModel new];
pm.name = @"Some Name";

// Model -> Dictionary
NSDictionary *dict = [pm toDictionary];

// Model -> JSON string
NSString *string = [pm toJSONString];

自定义 key mapping,支持跨层级:

objective-c 复制代码
{
  "orderId": 104,
  "orderDetails": {
    "name": "Product #1",
    "price": {
      "usd": 12.95
    }
  }
}

@interface OrderModel : JSONModel
@property (nonatomic) NSInteger id;
@property (nonatomic) NSString *productName;
@property (nonatomic) float price;
@end

@implementation OrderModel

+ (JSONKeyMapper *)keyMapper
{
  return [[JSONKeyMapper alloc] initWithModelToJSONDictionary:@{
    @"id": @"orderId",
    @"productName": @"orderDetails.name",
    @"price": @"orderDetails.price.usd"
  }];
}

@end

自动驼峰 key mapping:

objective-c 复制代码
{
  "order_id": 104,
  "order_product": "Product #1",
  "order_price": 12.95
}

@interface OrderModel : JSONModel
@property (nonatomic) NSInteger orderId;
@property (nonatomic) NSString *orderProduct;
@property (nonatomic) float orderPrice;
@end

@implementation OrderModel

+ (JSONKeyMapper *)keyMapper
{
  return [JSONKeyMapper mapperForSnakeCase];
}

@end

可选类型:

objective-c 复制代码
{
  "id": 123,
  "name": null,
  "price": 12.95
}

@interface ProductModel : JSONModel
@property (nonatomic) NSInteger id;
@property (nonatomic) NSString <Optional> *name;
@property (nonatomic) float price;
@property (nonatomic) NSNumber <Optional> *uuid;
@end

忽略属性:

objective-c 复制代码
{
  "id": 123,
  "name": null
}

@interface ProductModel : JSONModel
@property (nonatomic) NSInteger id;
@property (nonatomic) NSString <Ignore> *customProperty;
@end

在方法内动态设置可选:

objective-c 复制代码
{
  "id": null
}

@interface ProductModel : JSONModel
@property (nonatomic) NSInteger id;
@end

@implementation ProductModel

+ (BOOL)propertyIsOptional:(NSString *)propertyName
{
  if ([propertyName isEqualToString:@"id"])
    return YES;

  return NO;
}

@end

自定义转换:

objective-c 复制代码
@interface JSONValueTransformer (CustomTransformer)
@end

@implementation JSONValueTransformer (CustomTransformer)

- (NSDate *)NSDateFromNSString:(NSString *)string
{
  NSDateFormatter *formatter = [NSDateFormatter new];
  formatter.dateFormat = APIDateFormat;
  return [formatter dateFromString:string];
}

- (NSString *)JSONObjectFromNSDate:(NSDate *)date
{
  NSDateFormatter *formatter = [NSDateFormatter new];
  formatter.dateFormat = APIDateFormat;
  return [formatter stringFromDate:date];
}

@end

自定义 getters/setters:

objective-c 复制代码
@interface ProductModel : JSONModel
@property (nonatomic) NSInteger id;
@property (nonatomic) NSString *name;
@property (nonatomic) float price;
@property (nonatomic) NSLocale *locale;
@end

@implementation ProductModel

- (void)setLocaleWithNSString:(NSString *)string
{
  self.locale = [NSLocale localeWithLocaleIdentifier:string];
}

- (void)setLocaleWithNSDictionary:(NSDictionary *)dictionary
{
  self.locale = [NSLocale localeWithLocaleIdentifier:dictionary[@"identifier"]];
}

- (NSString *)JSONObjectForLocale
{
  return self.locale.localeIdentifier;
}

@end

自定义 JSON 校验:

objective-c 复制代码
@interface ProductModel : JSONModel
@property (nonatomic) NSInteger id;
@property (nonatomic) NSString *name;
@property (nonatomic) float price;
@property (nonatomic) NSLocale *locale;
@property (nonatomic) NSNumber <Ignore> *minNameLength;
@end

@implementation ProductModel

- (BOOL)validate:(NSError **)error
{
  if (![super validate:error])
    return NO;

  if (self.name.length < self.minNameLength.integerValue)
  {
    *error = [NSError errorWithDomain:@"me.mycompany.com" code:1 userInfo:nil];
    return NO;
  }

  return YES;
}

@end

Swift JSON <-> Model 方案

SwiftyJSON

Swift only,简化 JSON 处理,替代 JSONSerialization 的处理方式。让 JSON 直接转换为类似 Dictionary 的下标获取。通过 Optional 表达每个值。

所以,对比上面的 Objective-C 的几个方案,SwiftyJSON 就显得太弱了,只能说只是做到了更简单地读写 JSON 而已。

使用场景

直接读写 JSON 的值。

优势:

  • 更简洁、扁平化访问 JSON。
  • 提供更安全的类型支持。

限制:跟转 model 没有直接关系。

实践

基本使用:

swift 复制代码
let json = JSON(data: dataFromNetworking)
if let userName = json[0]["user"]["name"].string {
  //Now you got your value
}

// Getting an array of string from a JSON Array
let arrayNames =  json["users"].arrayValue.map {$0["name"].stringValue}

// Getting a string using a path to the element
let path: [JSONSubscriptType] = [1,"list",2,"name"]
let name = json[path].string
// Just the same
let name = json[1]["list"][2]["name"].string
// Alternatively
let name = json[1,"list",2,"name"].string

ObjectMapper

Swift only,JSON <-> Model。

使用场景

JSON <-> Mode

优势:

  • 支持所有 Swift 类型。
    • Int
    • Bool
    • Double
    • Float
    • String
    • RawRepresentable (Enums)
    • Array<Any>
    • Dictionary<String, Any>
    • Object<T: Mappable>
    • Array<T: Mappable>
    • Array<Array<T: Mappable>>
    • Set<T: Mappable>
    • Dictionary<String, T: Mappable>
    • Dictionary<String, Array<T: Mappable>>
    • Optionals of all the above
    • Implicitly Unwrapped Optionals of the above
  • 支持常量属性。
  • 支持继承。
  • 支持范型。
  • 易用,其自定义过程比使用 Codable 简单和灵活很多。

自定义:

  • 自定义属性映射。
  • 兼容属性的自定义值的配置。因为在 mapping 方法中,只会成功取出值后才会赋值,所以直接在属性上去定义的默认值仍有效。

限制/要求:

  • 需要遵循 Mappable 协议。
  • 对于变量属性使用 Mappable 协议;对于常量属性使用 ImmutableMappable 协议。
  • 变量属性使用 <- 运算符做映射;常量属性使用 >>> 做映射。
  • 枚举需要遵循 RawRepresentable

实践

基本使用,使用 <- 运算符建立 JSON 到属性变量的赋值。

swift 复制代码
// class Model 定义
class User: Mappable {
    var username: String?
    var age: Int?
    var weight: Double!
    var array: [Any]?
    var dictionary: [String : Any] = [:]
    var bestFriend: User?                       // Nested User object
    var friends: [User]?                        // Array of Users
    var birthday: Date?

    required init?(map: Map) {

    }

    // Mappable
    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
        weight      <- map["weight"]
        array       <- map["arr"]
        dictionary  <- map["dict"]
        bestFriend  <- map["best_friend"]
        friends     <- map["friends"]
        birthday    <- (map["birthday"], DateTransform())
    }
}

// struct Model 定义
struct Temperature: Mappable {
    var celsius: Double?
    var fahrenheit: Double?

    init?(map: Map) {

    }

    mutating func mapping(map: Map) {
        celsius   <- map["celsius"]
        fahrenheit  <- map["fahrenheit"]
    }
}

// 外部调用
// 使用 Model 的方法做转换
let user = User(JSONString: JSONString)
let JSONString = user.toJSONString(prettyPrint: true)
// 使用 Mapper 方法做转换
let user = Mapper<User>().map(JSONString: JSONString)
let JSONString = Mapper().toJSONString(user, prettyPrint: true)

支持常量属性:

遵循 ImmutableMappable 协议,

swift 复制代码
class User: ImmutableMappable {
  let id: Int
  let name: String?

  init(map: Map) throws {
    id   = try map.value("id")
    name = try? map.value("name")
  }

  func mapping(map: Map) {
    id   >>> map["id"]
    name >>> map["name"]
  }
}

// 使用
try User(JSONString: JSONString)

跨层级访问、key mapping 直接在 mapping 方法中进行:

swift 复制代码
"distance" : {
     "text" : "102 ft",
     "value" : 31
}

func mapping(map: Map) {
    distance <- map["distance.value"]
}

// 同样也支持数组
distance <- map["distances.0.value"]

// 对于带有 . 的 key 也有对应的处理方法。

// 忽略 key 中的 .
func mapping(map: Map) {
    identifier <- map["app.identifier", nested: false]
}
// 换成别的 nested key delimiter
func mapping(map: Map) {
    appName <- map["com.myapp.info->com.myapp.name", delimiter: "->"]
}

自定义转换:

swift 复制代码
birthday <- (map["birthday"], DateTransform())

// 定义自定义转换类型
public protocol TransformType {
    associatedtype Object
    associatedtype JSON

    func transformFromJSON(_ value: Any?) -> Object?
    func transformToJSON(_ value: Object?) -> JSON?
}

// 直接创建个转换变量
let transform = TransformOf<Int, String>(fromJSON: { (value: String?) -> Int? in
    // transform value from String? to Int?
    return Int(value!)
}, toJSON: { (value: Int?) -> String? in
    // transform value from Int? to String?
    if let value = value {
        return String(value)
    }
    return nil
})
id <- (map["id"], transform)
// 或直接一行写完
id <- (map["id"], TransformOf<Int, String>(fromJSON: { Int($0!) }, toJSON: { $0.map { String($0) } }))

总结

综上所述,在 Swift 中的序列化与反序列化方案选择可以参考:

  • 考虑依赖最少,原生支持:Codable。
  • 只是简单存取 JSON,不需要太多自定义特性:Codable。
  • 灵活自定义:ObjectMapper。
  • NSObject 子类:NSCoding、YYModel。

当然还要考虑项目中是否有现有的库依赖,如没有依赖 ObjectMapper,就优先考虑先使用其他的序列化方案了。

相关推荐
开心就好20251 天前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好20251 天前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao2 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy3 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy3 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode4 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农4 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
iceiceiceice5 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
ssshooter6 天前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust
二流小码农6 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos