iOS开发NSCoding和Codable的使用
在
iOS
实际开发中经常需要将数据保存在手机的磁盘中,下次启动App
的时候依然能保持之前使用的状态,这种存储数据称之为持久化存储,iOS
中持久化存储的方式非常多,有NSUserDeflault
,Core Data
,Realm
和NSCoding
,其中Core Data
和Realm
一般是针对数据比较复杂的情况,NSCoding
则是用在数据比较简单的场合,使用起来也非常方便。在Swift 4
开始,Apple
也提供了Coadble
的方式来序列化和反序列化数据,本文将会对NSCoding
和Coadble
进行详细的介绍,并比较二者之间的区别。
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
,实际是包含Encodable
和Decodable
协议,用来处理数据的序列化和反序列化,利⽤内置的 JSONEncoder
和 JSONDecoder
,在对象实例和JSON
表现之间进⾏转换变得⾮常简单,算是原生的JSON
和Model
互转的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
标准库中,像是String
,Int
, Double
, Date
或者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
,就需要理解枚举Codingkeys
和KeyedDecodingContainer
的作用,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)
代码解读:
- 在构造
JSON
中gift
这个key
所对应的value
时,里面用container.nestedContainer
这个可以嵌套层级关系的container
来构造关系,嵌套了一个toy
的key
。 -
toy
这个key
对应的value
是属性favoriteToy
。
本文介绍了二种
container
,当然Swift
还提供了unkeyedContainer
这种无key
的container
,本文在此不进行展开,有兴趣的可以自行查阅相关资料。
NSCoding
上文提到的Codable
协议可以使得Model
和JSON
相互转换,其实Codable
还支持XML
和PLists
,利用Codable
可以将JSON
和Model
转化成data
二进制数据,通过二进制数据这个桥梁再配合Codable
和Decodable
实现二者相互转化,当然利用data
也可以将转化后的数据进行持久化存储。在实际开发中对于一些轻巧的数据,要在本地进行持久化存储的话,可以采用NSCoding
进行归档存储。
使用
定义一个class
遵守NSObject
和NSCoding
协议,并实现encode(with aCoder:)
和init?(coder aDecoder:)
方法,这里采用了Keys
枚举类型作为encode
和decode
的key
,枚举类型编译器会有提示,避免手写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
能将轻巧的数据进行归档存储在磁盘上。 -
Codable
是Swift
中用来序列化和反序列化的协议,常用来实现JSON
和Model
之间的相互转化,可以进行自定义Key
来构建JSON
关系,比较灵活,是原生的解析JSON
的协议。