swift語言Codable?用法及原理詳解
Codable
Codable 本身就是個類型別名
typealias Codable = Decodable & Encodable
代表一個同時符合 Decodable 和 Encodable 協(xié)議的類型,即可解碼且可編碼的類型。
Codable 也可以代表蘋果為 Swift 開發(fā)的一套編解碼系統(tǒng),從 Swift 4 開始引入,包含了 Encoder 和 Decoder 協(xié)議和他們的兩個實現(xiàn) JSONEncoder
、JSONDecoder
和 PropertyListEncoder
、PropertyListDecoder
。其中 Codable 及其相關(guān)協(xié)議放在了標(biāo)準(zhǔn)庫中,而具體的 Encoder、Decoder 類放在了 Foundation
框架中。
Codable 的用法
Codable 是用來做系統(tǒng)自身數(shù)據(jù)結(jié)構(gòu)和外部公共數(shù)據(jù)結(jié)構(gòu)做轉(zhuǎn)換的。系統(tǒng)內(nèi)部數(shù)據(jù)結(jié)構(gòu)可以是基礎(chǔ)類型、結(jié)構(gòu)體、枚舉、類等,外部公共數(shù)據(jù)結(jié)構(gòu)可以是 JSON、XML 等。
JSON 和 模型的相互轉(zhuǎn)換
用 Objective-C 做 JSON 和模型轉(zhuǎn)換時,一般要使用一些第三方庫,這些第三方庫基本上都是利用了 Objective-C Runtime 的強(qiáng)大特性來實現(xiàn) JSON 和模型互轉(zhuǎn)的。
但是 Swift 是一門靜態(tài)語言,本身是沒有像 Objective-C 那樣的動態(tài) Runtime 的。雖然在 Swift 中也可以通過繼承 NSObject 的方式,來使用基于 OC Runtime 的 JSON 模型互轉(zhuǎn)方案。但是這樣就很不 Swift,也放棄了 Swift 作為一門靜態(tài)語言的高性能,等于說自己降低了整個項目的運行性能,這是無法忍受的。
好在蘋果提供了 JSONEncoder
和 JSONDecoder
這兩個結(jié)構(gòu)體來方便得在 JSON 數(shù)據(jù)和自定義模型之間互相轉(zhuǎn)換。蘋果可以利用一些系統(tǒng)私有的機(jī)制來實現(xiàn)轉(zhuǎn)換,而不需要通過 OC Runtime
。
只要讓自己的數(shù)據(jù)類型符合 Codable 協(xié)議,就可以用系統(tǒng)提供的編解碼器進(jìn)行編解碼。
struct User: Codable { var name: String var age: Int }
具體編解碼代碼如下:
解碼(JSON Data -> Model):
let json = """ { "name": "zhangsan", "age": 25 } """.data(using: .utf8)! let user = JSONDecoder().decode(User.self, from: json)
編碼(Model -> JSON Data):
let data = JSONEncoder().encode(user)
Codable 支持的數(shù)據(jù)類型
基礎(chǔ)數(shù)據(jù)類型
在 Swift 標(biāo)準(zhǔn)庫的聲明文件中可以看到,基礎(chǔ)類型都通過 extension
實現(xiàn)了 Codable
協(xié)議。
對于基礎(chǔ)類型的屬性,JSONEncoder 和 JSONDecoder 都可以正確的處理。
Date
JSONEncoder
提供了 dateEncodingStrategy
屬性來指定日期編碼策略。 同樣 JSONDecoder
提供了 dateDecodingStrategy
屬性。
就拿 dateDecodingStrategy
為例,它是一個枚舉類型。枚舉類型有以下幾個 case:
case 名 | 作用 |
---|---|
case deferredToDate | 默認(rèn)的 case |
case iso8601 | 按照日期的 ios8601 標(biāo)準(zhǔn)來解碼日期 |
case formatted(DateFormatter) | 自定義日期解碼策略,需要提供一個 DateFormatter 對象 |
case custom((_ decoder: Decoder) throws -> Date) | 自定義日期解碼策略,需要提供一個 Decoder -> Date 的閉包 |
通常使用比較多的就是 .iso8601
了,因為后端返回日期通常都是已 ios8601 格式返回的。只要 JSON 中的日期是 ios8601 規(guī)范的字符串,只要設(shè)置一行代碼就能讓 JSONDecoder 完成日期的解碼。
struct User: Codable { var name: String var age: Int var birthday: Date } let json = """ { "name": "zhangsan", "age": 25, "birthday": "2022-09-12T10:25:41+00:00" } """.data(using: .utf8)! let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let user = decoder.decode(User.self, from: json) // user.birthday 正確解碼為 Date 類型
嵌套對象
在自定義模型中嵌套對象的時候,只要這個嵌套對象也符合 Codable 協(xié)議,那整個對象就可以正常使用 JSONEncoder
和 JSONDecoder
編解碼。
struct UserInfo: Codable { var name: String var age: Int } struct User: Codable { var info: UserInfo }
枚舉
枚舉類型必須它的 RawValue 的類型是可解碼的,并且 RawValue 的類型和 JSON 字段類型對應(yīng),即可正確解碼。
自定義 CodingKeys
自定義 CodingKeys 主要是兩個目的
- 當(dāng)數(shù)據(jù)類型屬性名和 JSON 中字段名不同時,做 key 的映射。
- 通過在不添加某些字段的 case,來跳過某些字段的編解碼過程。
struct User: Codable { var name: String var age: Int var birthday: Date? enum CodingKeys: String, CodingKey { case name = "userName" case age = "userAge" } }
CodingKeys 必須是一個 RawValue 為 String 類型的枚舉,并符合 CodingKey
協(xié)議。以上代碼實現(xiàn)的效果為,為 name 和 age 字段做了 key 映射,讓編解碼過程中不包含 birthday 字段。
Codable 的原理
了解了 Codable 的用法,下面我們來看一看 Codable 的原理。
Decodable 協(xié)議
由于編碼和解碼的原理差不多只是方向不同,我們僅探索用的更多的解碼過程。
如果想讓一個對象支持解碼應(yīng)該怎么做呢,當(dāng)然是符合 Decodable 協(xié)議。我們先看看一個對象符合 Decodable 協(xié)議需要做哪些事情。
Decodable
協(xié)議的定義如下:
public protocol Decodable { init(from decoder: Decoder) throws }
也就是說只要實現(xiàn)一個傳入 Decoder 參數(shù)的初始化方法,于是我們自己來實現(xiàn) User。
struct User: Decodable { var name: String var age: Int init(from decoder: Decoder) throws { } }
現(xiàn)在要來看看怎樣讓 User 的兩個屬性的值能從 Decoder 這個對象得到。
查看 Decoder
的定義,它是一個協(xié)議。 有兩個屬性:
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
會發(fā)現(xiàn)這三個方法返回的都是 XXXContainer,從字面上理解是個容器,容器里面一定是容納了某些東西。
Container
再查看這些 Container 的定義,會發(fā)現(xiàn)里面都有一系列 decode... 方法,來對各種類型進(jìn)行 decode。
一共有三種類型的 Container:
Container 類型 | 作用 |
---|---|
SingleValueDecodingContainer | 代表容器中只保存了一個值 |
KeyedDecodingContainer | 代表容器中保存的數(shù)據(jù)是按照鍵值對的形式保存的 |
UnkeyedDecodingContainer | 代表容器中保存的數(shù)據(jù)是沒有鍵的,也就是說,保存的數(shù)據(jù)是一個數(shù)組 |
回到上面 User 的例子,JSON 數(shù)據(jù)如下:
{ "user": "zhangsan", "age": 25 }
這種數(shù)據(jù)顯然是鍵值對,因此要用 KeyedDecodingContainer 來取數(shù)據(jù)。KeyedDecodingContainer 應(yīng)該是最常用的 Container 了。
struct User: Decodable { var name: String var age: Int init(from decoder: Decoder) throws { decoder.container(keyedBy: <#T##CodingKey.Protocol#>) } }
參數(shù)需要傳一個符合 CodingKey 協(xié)議的對象的類型,于是這里必須自己實現(xiàn) CodingKeys 枚舉,并把 CodingKeys.self 傳入?yún)?shù)。
struct User: Decodable { var name: String var age: Int enum CodingKeys: String, CodingKey { case name case age } init(from decoder: Decoder) throws { let container = decoder.container(keyedBy: CodingKeys.self) } }
然后就可以從 container 中取數(shù)據(jù)出來賦給自身的屬性。由于這幾個方法都會拋出異常,因此都要加上 try
。
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) age = try container.decode(Int.self, forKey: .age) }
同樣的,我們也可以實現(xiàn)出編碼。這時把 User 實現(xiàn)的協(xié)議改成 Codable
。
struct User: Codable { var name: String var age: Int enum CodingKeys: String, CodingKey { case name case age } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) age = try container.decode(Int.self, forKey: .age) } func encode(to encoder: Encoder) throws { var encoder = encoder.container(keyedBy: CodingKeys.self) try encoder.encode(name, forKey: .name) try encoder.encode(age, forKey: .age) } }
編碼的過程就是和解碼反過來,因為是鍵值對,從 encoder 中拿到 KeyedEncoderContainer
,然后調(diào)用 encode 方法把屬性的數(shù)據(jù)編碼到 container 中,然后由 JSONEncoder
來處理接下來的事情。
接下來我們好奇的是,Container 中的數(shù)據(jù)是怎么保存的,Container 中的數(shù)據(jù)和 JSON 又是怎么互相轉(zhuǎn)換的。
核心原理分析(Container <--> JSON)
JSONDecoder 的解碼過程
從 JSONDecoder().decode(User.self, from: json)
這句開始分析。打開 swift-corelibs-foundation 中 JSONDecoder
的源碼。
// 1 var parser = JSONParser(bytes: Array(data)) let json = try parser.parse() // 2 return try JSONDecoderImpl(userInfo: self.userInfo, from: json, codingPath: [], options: self.options) .unwrap(as: T.self) // 3
decode 方法的實現(xiàn)主要是這三行代碼。
- 先把 data 轉(zhuǎn)化為一個類型為
JSONValue
的 json 對象。 - 然后構(gòu)造一個 JSONDecoderImpl 對象
- 調(diào)用 JSONDecoderImpl 對象的
unwrap
方法得到要轉(zhuǎn)換成的對象。
查看 JSONValue
的定義,它通過枚舉嵌套把 JSON 的類型定義了出來。具體的數(shù)據(jù)通過關(guān)聯(lián)值攜帶在了這個枚舉類型中。
enum JSONValue: Equatable { case string(String) case number(String) case bool(Bool) case null case array([JSONValue]) case object([String: JSONValue]) }
在獲取 KeyedDecodingContainer 的時候也就是通過 JSONValue 構(gòu)建 Container 對象。
// 這里 self.json 是保存在 JSONDecoderImpl 中的 JSONValue 類型 switch self.json { case .object(let dictionary): // JSONValue 和 .object 這個 case 匹配,取出字典數(shù)據(jù) let container = KeyedContainer<Key>( impl: self, codingPath: codingPath, dictionary: dictionary // 傳入字典數(shù)據(jù) ) return KeyedDecodingContainer(container)
可以看到,KeyedDecodingContainer
只有當(dāng) self.json
匹配為字典時才能正確創(chuàng)建。數(shù)據(jù)在里面以 let dictionary: [String: JSONValue]
形式保存。
再看其他代碼可以發(fā)現(xiàn):
SingleValueContainer
就是直接存了一個 let value: JSONValue
在里面。
UnkeyedDecodingContainer
則是存了一個數(shù)組 let array: [JSONValue]
。
因此在 Container 調(diào)用 decode
方法獲取數(shù)據(jù)時,就是根據(jù)參數(shù) key 和類型從自身保存的數(shù)據(jù)中獲取數(shù)據(jù)。這個源碼很簡單,看一下就明白了。
最后一步的 unwrap 方法,通過源碼可以看到,最終調(diào)用的就是對象自己實現(xiàn)的 init(from decoder: Decoder)
方法
因此可以得出 JSON -> Model 的步驟如下:
- JSONParser 對傳入的二進(jìn)制 JSON data 進(jìn)行解析,解析為 JSONValue 對象。
- 構(gòu)建 JSONDecoderImpl,將相關(guān)的數(shù)據(jù)保存在里面。
- 調(diào)用 JSONDecoderImpl 的 unwrap 方法,開始調(diào)用對象實現(xiàn)的
init(from: decoder: Decoder)
方法 - 在 ``init(from: decoder: Decoder)` 方法中,首先根據(jù)數(shù)據(jù)類型獲取對應(yīng)的 Container。
- 調(diào)用 Container 的 decodeXXX 方法得到具體的值賦值給屬性。
Model -> JSON 的步驟也是差不多的,只是方向反過來,有興趣可以自己看一下源碼。
在 Swift 的 JSON 轉(zhuǎn)模型方法中,通過觀察 Github 上的開源庫可以發(fā)現(xiàn)一共有三種實現(xiàn)方案:
- Objective-C Runtime 一眾本身就是 OC 開發(fā)的庫基本都用的這個方案,比如 YYModel,這種方案使用起來非常簡單,代碼非常少,但不符合 Swift。
- Key 映射 比如 ObjectMapper 就是這種,這種的缺點是每個對象都要寫一大堆映射代碼,比較麻煩
- 利用對象底層內(nèi)存布局 SwiftyJSON 就屬于這種,這種方法使用起來一樣很方便,但是依賴蘋果的私有代碼,蘋果如果調(diào)整了內(nèi)部實現(xiàn)就會失效。
通過上面分析 Codable 原理發(fā)現(xiàn),Codable 基本上就是 Key 映射的方案,只不過編譯器幫我們自動合成了很多代碼來讓我們使用起來一樣可以非常簡單。由于編譯器不會幫第三方庫合成代碼,因此 Codable 秒殺了一眾基于 key 映射實現(xiàn)的第三方庫。
編譯器幫我們做了什么?
我們發(fā)現(xiàn),只要讓自己的對象符合 Codable 協(xié)議,就可以正常用 JSONEncoder
和 JSONDecoder
編解碼,并不需要實現(xiàn)協(xié)議中定義的方法。
那是因為編譯器幫我們生成了。這種編譯器合成代碼在很多地方都會用到,例如為結(jié)構(gòu)體和枚舉自動合成實現(xiàn) Equatable 和 Hashable 的代碼,為枚舉合成實現(xiàn) CaseIterable 的代碼等。
上面的 User 例子,編譯器為我們合成的代碼如下:
struct User: Codable { var name: String var age: Int // 編譯器合成 enum CodingKeys: String, CodingKey { case name case age } // 編譯器合成 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) age = try container.decode(Int.self, forKey: .age) } // 編譯器合成 func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(age, forKey: .age) } }
可以見到,編譯器自動合成了 CodingKeys 枚舉的定義,并合成了實現(xiàn) Encodable 和 Decodable 協(xié)議的代碼。這給開發(fā)人員提供了方便。
默認(rèn)值問題
編譯器自動生成的編解碼實現(xiàn)有個問題就是不支持默認(rèn)值。如果需要支持默認(rèn)值就需要自己來用 decodeIfPresent
來實現(xiàn):
struct User: Decodable { var name: String var age: Int enum CodingKeys: String, CodingKey { case name case age } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 0 } }
但是這樣每個結(jié)構(gòu)體都要自己實現(xiàn)一次,非常麻煩。其實這個網(wǎng)上已經(jīng)有很多文章在說了,就是用 @propertyWrapper
屬性包裝器來解決這個問題。
屬性包裝器 @propertyWrapper
屬性包裝器用來給屬性和定義屬性的結(jié)構(gòu)之間包裝一層,用來實現(xiàn)一些通用的 setter 和 getter 邏輯或初始化邏輯等。
例如對于 Int 型,可以如下定義屬性包裝器。
@propertyWrapper public struct DefaultInt: Codable { public var wrappedValue: Int public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = (try? container.decode(BaseType.self)) ?? 0 } public func encode(to encoder: Encoder) throws { try wrappedValue.encode(to: encoder) } }
以上代碼實現(xiàn)了 init(from decoder: Decoder)
方法來為屬性在解碼失敗時提供一個默認(rèn)值 0。實現(xiàn) encode(to encoder: Encoder)
是為了編碼時直接編碼內(nèi)部值而不是編碼整個屬性包裝類型。
其它的很多基礎(chǔ)類型都是一樣的邏輯,為了避免重復(fù)代碼,可以用范型來統(tǒng)一實現(xiàn)。
public protocol HasDefaultValue { static var defaultValue: Self { get set } } @propertyWrapper public struct DefaultBaseType<BaseType: Codable & HasDefaultValue>: Codable { public var wrappedValue: BaseType public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = (try? container.decode(BaseType.self)) ?? BaseType.defaultValue } public func encode(to encoder: Encoder) throws { try wrappedValue.encode(to: encoder) } }
然后可以考慮用類型別名來定義出各個類型的屬性包裝關(guān)鍵字。因為如果包含 <
或 .
等字符,寫起來會比較麻煩。
typealias DefaultInt = DefaultBaseType<Int> typealias DefaultString = DefaultBaseType<String>
但是有些類型需要特殊實現(xiàn)一下。
枚舉
枚舉類型可以利用 rawValue 來進(jìn)行數(shù)據(jù)和類型相互轉(zhuǎn)換。
@propertyWrapper public struct DefaultIntEnum<Value: RawRepresentable & HasDefaultEnumValue>: Codable where Value.RawValue == Int { private var intValue = Value.defaultValue.rawValue public var wrappedValue: Value { get { Value(rawValue: intValue)! } set { intValue = newValue.rawValue } } public init() { } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() intValue = (try? container.decode(Int.self)) ?? Value.defaultValue.rawValue } public func encode(to encoder: Encoder) throws { try intValue.encode(to: encoder) } }
數(shù)組
由于數(shù)組需要通過 UnkeyedDecodingContainer 拿數(shù)據(jù),需要單獨特殊處理。
@propertyWrapper public struct DefaultArray<Value: Codable>: Codable { public var wrappedValue: [Value] public init() { wrappedValue = [] } public init(wrappedValue: [Value]) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() var results = [Value]() while !container.isAtEnd { let value = try container.decode(Value.self) results.append(value) } wrappedValue = results } public func encode(to encoder: Encoder) throws { try wrappedValue.encode(to: encoder) } }
對象
因為對象的結(jié)構(gòu)都是不一樣的,沒法給出一個的默認(rèn)值。因此設(shè)計了一個 EmptyInitializable
協(xié)議,里面只有一個無參數(shù)的初始化方法。
public protocol EmptyInitializable { init() }
需要提供默認(rèn)值的對象可以實現(xiàn)這個協(xié)議。不過這里需要權(quán)衡一下,如果對內(nèi)存空間占用有比較高的要求,用可選值可能是更好的方案,因為一個空對象占用的空間和有數(shù)據(jù)的對象占用的空間是一樣多的。
屬性包裝器的使用
使用屬性包裝器封裝各個類型后,只要像這樣使用就可以了,decode 的時候就如果不存在對應(yīng)字段數(shù)據(jù)屬性就會初始化為默認(rèn)值。
struct User { @DefaultString var name: String @DefaultInt var age: Int }
我簡單封裝了一個庫,目前我們的新 Swift 項目在使用,完整代碼在這里: github.com/liuduoios/C…
參考資料:
《the swift programming language》
《Advanced Swift》
swift-corelibs-foundation 源碼
以上就是swift語言Codable 用法及原理詳解的詳細(xì)內(nèi)容,更多關(guān)于swift Codable用法原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Swift實現(xiàn)可自定義分頁寬度的UIScrollView
這篇文章主要為大家詳細(xì)介紹了Swift實現(xiàn)可自定義分頁寬度的UIScrollView,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07Swift TableView實現(xiàn)凍結(jié)窗格功能
這篇文章主要為大家詳細(xì)介紹了Swift TableView實現(xiàn)凍結(jié)窗格功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11swift中AnyObject和Any的介紹與區(qū)別詳解
雖然使用swift開發(fā)了一段時間,但是感覺對一些基礎(chǔ)的東西了解不是比較透徹,在查詢了許多資料以后還是打算自己動手記錄一下,下面這篇文章主要給大家介紹了關(guān)于swift中AnyObject和Any的介紹與區(qū)別的相關(guān)資料,需要的朋友可以參考下。2017-12-12