美文网首页
iOS开发NSCoding和Codable的使用

iOS开发NSCoding和Codable的使用

作者: MambaYong | 来源:发表于2021-09-23 17:01 被阅读0次

    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 <Foundation/Foundation.h>
    @class ProviceArchiverModel,Province,City,Country
    @interface ProviceArchiverModel : NSObject <NSCoding>
    @property (nonatomic,strong)NSArray<Province *> *body;
    @end
    
    @interface Province : NSObject <NSCoding>
    @property (nonatomic,copy)NSString *addrCode;
    @property (nonatomic,copy)NSString *addrName;
    @property (nonatomic,strong)NSArray <RemoteCity *> *subAddrs;
    @end
    
    @interface City : NSObject <NSCoding>
    @property (nonatomic,copy)NSString *addrCode;
    @property (nonatomic,copy)NSString *addrName;
    @property (nonatomic,strong)NSArray<Country *> *subAddrs;
    @end
    
    @interface Country : NSObject <NSCoding>
    @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的使用

          本文链接:https://www.haomeiwen.com/subject/jguwgltx.html