iOS开发NSCoding和Codable的使用

iOS实际开发中经常需要将数据保存在手机的磁盘中,下次启动App的时候依然能保持之前使用的状态,这种存储数据称之为持久化存储,iOS中持久化存储的方式非常多,有NSUserDeflaultCore DataRealmNSCoding,其中Core DataRealm一般是针对数据比较复杂的情况,NSCoding则是用在数据比较简单的场合,使用起来也非常方便。在Swift 4开始,Apple也提供了Coadble的方式来序列化和反序列化数据,本文将会对NSCodingCoadble进行详细的介绍,并比较二者之间的区别。

Codable

在开发中和后台返回的JSON格式数据打交道是家常便饭的事情,在OC时代,将JSON格式数据转成模型是一件很容易的事情,利用NSJSONSerialization将一个字符串解析成NSDictionary后,在配合KVC,就能将JSON格式的数据转化成Model数据使用,后面的NSCoding例子会进行相关演示;Swift语言由于对类型要求非常严格,使用NSJSONSerialization解析完一个JSON字符串后得到的是Any?类型,这样在将Any?类型的数据转为模型时需要层层的判断,非常麻烦!

// jsonString
{"menu": { "id": "file", 
                 "value": "File", 
                 "popup": { "menuitem": [ 
                                 {"value": "New", "onclick": "CreateNewDoc()"},
                                 {"value": "Open", "onclick": "OpenDoc()"},
                                 {"value": "Close", "onclick": "CloseDoc()"}
     ] 
   } 
}}
let json: Any = try! JSONSerialization.jsonObject( with: jsonString.data(using: .utf8, allowLossyConversion: true)!, options: [])

如果想要解析里面的Value的话就会写出如下的恶心代码:

if let jsonDic = json as? NSDictionary,
   let menu = jsonDic["menu"] as? [String: Any],
   let popup = menu["popup"], 
   let popupDic = popup as? [String: Any],
   let menuItems = popupDic["menuitem"],
   let menuItemsArr = menuItems as? [Any],
   let item0 = menuItemsArr[0] as? [String: Any], 
   let value = item0["value"] 
  {  
     print(value)
  }

Swift目前加入了Codable,实际是包含EncodableDecodable协议,用来处理数据的序列化和反序列化,利⽤内置的 JSONEncoderJSONDecoder,在对象实例和JSON表现之间进⾏转换变得⾮常简单,算是原生的JSONModel互转的API,使用起来非常的方便。

使用:

定义一个Struct满足Decodable协议。

struct Obj: Codable {
       let menu: Menu 
       struct Menu: Codable { 
                            let id: String 
                            let value: String 
                            let popup: Popup 
                  }
        struct Popup: Codable { 
                            let menuItem: [MenuItem] 
                            enum CodingKeys: String, CodingKey { 
                                   case menuItem = "menuitem"
                             } 
                  }
        struct MenuItem: Codable { 
                            let value: String 
                            let onClick: String 
                            enum CodingKeys: String, CodingKey { 
                                   case value 
                                   case onClick = "onclick"
                             }
                  }
 }
// 使用
let data = jsonString.data(using: .utf8)! 
do {
      let obj = try JSONDecoder().decode(Obj.self, from: data) 
      let value = obj.menu.popup.menuItem[0].value 
      print(value) 
} catch { 
      print("出错啦:\(error)")
 }

只要类或者结构体中所有成员都实现了Coadble,这个类型也就自动满足Coadble了,在 Swift标准库中,像是StringIntDoubleDate或者URL 这样的类型是默认就实现 了Codable的,因此我们可以简单地基于这些常⻅类型来构建更复杂的 Codable类型,利用 CodingKeys枚举,可以实现当属性名和JSON的对应不上时进行映射,当然我们也可以像下面一样进行手动实现。

struct Obj: Codable {
       let menu: Menu 
       struct Menu: Codable { 
                            let id: String 
                            let value: String 
                            let popup: Popup 
                  }
        struct Popup: Codable { 
                            let menuItem: [MenuItem] 
                  }
        struct MenuItem: Codable { 
                            let value: String 
                            let onClick: String 
                  }
 }
extension Obj {
    struct CodingData: Codable {
         struct Container: Codable {
                            let id: String 
                            let value: String 
                            let popup: Popup 
                  }
        struct Popup: Codable { 
                            let menuitem: [MenuItem]  // 按钮JSON的key进行构建
                  }
        struct MenuItem: Codable { 
                            let value: String 
                            let onclick: String // 按钮JSON的key进行构建
                  }
         }
      var objData: Container
    }
}
extension Obj.CodingData {
    var obj: Obj {
        return Obj(
            id: objData.id,
            age: objData.value,
             popup: objData.popup
        )
    }
}

使用时。

let data = jsonString.data(using: .utf8)! 
do {
      let codingData = try JSONDecoder().decode(Obj.CodingData.self, from: data) 
      let obj = codingData.user
      let value = obj.menu.popup.menuItem[0].value 
      print(value) 
} catch { 
      print("出错啦:\(error)")
 }

Codabe深入探析

现在有一本书简介的JSON字符串。

let jsonString = """{"title": "War and Peace: A protocol oriented approach to diplomacy "author": "A. Keed Decoder"}"""

创建一个Book结构体。

struct Book: Decodable {
  var title: String
  var author: String
}
if let data = jsonString.data(using: .utf8) {
  let decoder = JSONDecoder()
  if let book = try? decoder.decode(Book.self, from: data) {
    print(book.title) // "War and Peace: A protocol oriented approach to diplomacy"
  }
}

上面的Book结构体遵守Decodable协议,其实编译器帮我们做了如下事情:

  • 添加了一个 init(from decoder:的初始化方法。

在初始化方法中出现了decoder调用container属性,返回的其实是一个KeyedDecodingContainer类型,理解KeyedDecodingContainer是理解Codable的关键。

  • 定义了一个CodingKeys枚举,遵守CodingKey协议。
struct Book: Decodable {
    var title: String
    var author: String
    // 下面是编译器自动帮忙做的事情
    init(from decoder: Decoder) throws {
        let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
        title = try keyedContainer.decode(String.self, forKey: .title)
        author = try keyedContainer.decode(String.self, forKey: .author)
    }
    enum CodingKeys: String, CodingKey {
        case title
        case author
    }
}

KeyedDecodingContainer理解

要弄清Codable,就需要理解枚举CodingkeysKeyedDecodingContainer的作用,Codingkeys是用来定义JSON字符串中的key的(包括和Model之间的映射关系),KeyedDecodingContainer用来将Codingkeys定义的key按照一定的嵌套关系组织起来,因为JSON里的key是有层级关系的,这由KeyedDecodingContainer负责,可以看做是一个dictionary用来存放encode的属性。

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : "Teddy Bear"
}
import Foundation
struct Toy: Codable {
  var name: String
}
struct Employee: Encodable {
  var name: String
  var id: Int
  var favoriteToy: Toy    
  enum CodingKeys: CodingKey {
    case name, id, gift
  }
    
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(id, forKey: .id)
    try container.encode(favoriteToy.name, forKey: .gift)
  }
}

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)
    let gift = try container.decode(String.self, forKey: .gift)
    favoriteToy = Toy(name: gift)
  }
}

let toy = Toy(name: "Teddy Bear")
let employee = Employee(name: "John Appleseed", id: 7, favoriteToy: toy)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(employee)
let string = String(data: data, encoding: .utf8)! // "{"name":"John Appleseed","id":7,"gift":"Teddy Bear"}"
let sameEmployee = try decoder.decode(Employee.self, from: data)

代码解读:

  • decoder.container生成的是顶级KeyedDecodingContainer,也即JSON中的key不存在嵌套关系。
  • 由于是顶级container构造的key关系,所以gift对应的是Toy中的name属性,而不是Toy本身。

Nested keyed containers

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
}
import Foundation
struct Toy: Codable {
  var name: String
}
struct Employee: Encodable {
  var name: String
  var id: Int
  var favoriteToy: Toy    
  enum CodingKeys: CodingKey {
    case name, id, gift
  }    
  enum GiftKeys: CodingKey {
    case toy
  }    
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(id, forKey: .id)
    var giftContainer = container.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
    try giftContainer.encode(favoriteToy, forKey: .toy)
  }
}
let toy = Toy(name: "Teddy Bear")
let employee = Employee(name: "John Appleseed", id: 7, favoriteToy: toy)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let nestedData = try encoder.encode(employee)
let nestedString = String(data: nestedData, encoding: .utf8)! //"{"name":"John Appleseed","id":7,"gift":{"toy":{"name":"Teddy Bear"}}}"
extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)
    let giftContainer =  try container.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift) // 有嵌套关系
    favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
  }
}
let sameEmployee = try decoder.decode(Employee.self, from: nestedData)

代码解读:

  • 在构造JSONgift这个key所对应的value时,里面用container.nestedContainer这个可以嵌套层级关系的container来构造关系,嵌套了一个toykey
  • toy这个key对应的value是属性favoriteToy

本文介绍了二种container,当然Swift还提供了unkeyedContainer这种无keycontainer,本文在此不进行展开,有兴趣的可以自行查阅相关资料。

NSCoding

上文提到的Codable协议可以使得ModelJSON相互转换,其实Codable还支持XMLPLists,利用Codable可以将JSONModel转化成data二进制数据,通过二进制数据这个桥梁再配合CodableDecodable实现二者相互转化,当然利用data也可以将转化后的数据进行持久化存储。在实际开发中对于一些轻巧的数据,要在本地进行持久化存储的话,可以采用NSCoding进行归档存储。

使用

定义一个class遵守NSObjectNSCoding协议,并实现encode(with aCoder:)init?(coder aDecoder:)方法,这里采用了Keys枚举类型作为encodedecodekey,枚举类型编译器会有提示,避免手写key时出现错误。

enum Keys: String {
  case title = "Title"
  case rating = "Rating"
}
import Foundation
class ScaryCreatureData: NSObject,NSCoding,NSSecureCoding {
  var title = ""
  var rating: Float = 0  
  init(title: String, rating: Float) {
    super.init()
    self.title = title
    self.rating = rating
  }
         // NSCoding需要实现的协议
  func encode(with aCoder: NSCoder) {
        aCoder.encode(title, forKey: Keys.title.rawValue)
        aCoder.encode(rating, forKey: Keys.rating.rawValue)
  }
  required convenience init?(coder aDecoder: NSCoder) {
        //便捷初始化器必须调用必要初始化器
        let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String
        let rating = aDecoder.decodeFloat(forKey: Keys.rating.rawValue)
        self.init(title: title, rating: rating)
  }
}

encode归档

    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let documentsDirectoryURL = paths.first!.appendingPathComponent("PrivateDocuments")
    // 创建存放数据的文件夹
    do {
      try FileManager.default.createDirectory(at: documentsDirectoryURL,
                                              withIntermediateDirectories: true,
                                              attributes: nil)
    } catch {
      print("Couldn't create directory")
    }
     let dataURL = documentsDirectoryURL.appendingPathComponent("Data.plist")
     var archiveData = ScaryCreatureData(title: "mamba", rating: 12.0)
     // 归档
     let codedData = try! NSKeyedArchiver.archivedData(withRootObject: archiveData,
                                                      requiringSecureCoding: true)
    // 写入文件
    do {
      try codedData.write(to: dataURL)
    } catch {
      print("Couldn't write to save file: " + error.localizedDescription)
    }
  }

decode解档

  var data:ScaryCreatureData?{
     get {
      guard let codedData = try? Data(contentsOf: dataURL) else { return nil }
      _data = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(codedData) as?
          ScaryCreatureData 
      return _data
    }
     set {
      _data = newValue
   }
}

利用MJExtension实现模型数组的归档

本次代码示例采用OC实现,下面的JSON是全国省市区的数据,可以利用NSCoding归档的方式保存在本地。

  {
    "body": 
            [
                {
                    "addrCode": "320000",
                    "addrName": "江苏省",
                    "subAddrs": [
                        {
                            "addrCode": "320200",
                            "addrName": "无锡市",
                            "subAddrs": [
                                {
                                    "addrCode": "320201",
                                    "addrName": "市辖区"
                                },
                                {
                                    "addrCode": "320205",
                                    "addrName": "锡山区"
                                },
                                {
                                    "addrCode": "320206",
                                    "addrName": "惠山区"
                                }
                                        ]
                        }
                         ...
                                 ]
                 }
                 ...
             ]  
              ...
}

首先建立对应的Model

#import 
@class ProviceArchiverModel,Province,City,Country
@interface ProviceArchiverModel : NSObject 
@property (nonatomic,strong)NSArray *body;
@end

@interface Province : NSObject 
@property (nonatomic,copy)NSString *addrCode;
@property (nonatomic,copy)NSString *addrName;
@property (nonatomic,strong)NSArray  *subAddrs;
@end

@interface City : NSObject 
@property (nonatomic,copy)NSString *addrCode;
@property (nonatomic,copy)NSString *addrName;
@property (nonatomic,strong)NSArray *subAddrs;
@end

@interface Country : NSObject 
@property (nonatomic,copy)NSString *addrCode;
@property (nonatomic,copy)NSString *addrName;
@end

添加Model.m文件实现。

#import "ProviceArchiverModel.h"
#import "MJExtension.h"
// 添加模型中的映射关系
@implementation ProviceArchiverModel
+ (NSDictionary *)mj_objectClassInArray{
    return @{@"body":@"Province"};
}
// 利用此宏实现NSCoding协议
MJCodingImplementation
@end

@implementation Province
+ (NSDictionary *)mj_objectClassInArray{
     return @{@"subAddrs":@"RemoteCity"};
}
// 利用此宏实现NSCoding协议
MJCodingImplementation
@end

@implementation City
+ (NSDictionary *)mj_objectClassInArray{
     return @{@"subAddrs":@"Country"};
}
// 利用此宏实现NSCoding协议
MJCodingImplementation
@end

JSON数据转模型并归档。

   NSData *jsonData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
   NSDictionary * dic = [NSDictionary dictionaryWithDictionary:[NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil]];
   ProviceArchiverModel *model = [ProviceArchiverModel mj_objectWithKeyValues:dic];
   NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"ChinaProviceAndCity.data"];
   [NSKeyedArchiver archiveRootObject:proviceArchiverModel toFile:file];

解档

    NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"ChinaProviceAndCity.data"];
    ProviceArchiverModel *model = [NSKeyedUnarchiver unarchiveObjectWithFile:file];

总结:

  • NSCoding能将轻巧的数据进行归档存储在磁盘上。
  • CodableSwift中用来序列化和反序列化的协议,常用来实现JSONModel之间的相互转化,可以进行自定义Key来构建JSON关系,比较灵活,是原生的解析JSON的协议。

你可能感兴趣的:(iOS开发NSCoding和Codable的使用)