???github SmartCodable 看起來還不錯申屹?給個star?吧焙贷,急需支持???
SmartCodable - Swift數(shù)據(jù)解析的智能解決方案
SmartCodable 是一個基于Swift的Codable協(xié)議的數(shù)據(jù)解析庫拍皮,旨在提供更為強大和靈活的解析能力歹叮。通過優(yōu)化和擴展Codable的標準功能跑杭,SmartCodable 有效地解決了傳統(tǒng)解析過程中的常見問題,并提高了解析的容錯性和靈活性咆耿。
為何選擇SmartCodable?
在使用標準的Codable進行數(shù)據(jù)解析時德谅,開發(fā)者常常會遇到諸如鍵不存在、類型不匹配或值為null等問題萨螺,這些問題往往會導致整個解析過程的失敗窄做,并拋出異常。SmartCodable 針對這些挑戰(zhàn)提供了智能化的解決方案慰技,確保解析過程的健壯性和流暢性椭盏。
主要特性
- 增強的錯誤處理:當遇到鍵不存在、類型不匹配或值為null等問題時吻商,不會立即中斷解析掏颊,而是提供了更為靈活的處理選項。
- 值類型轉(zhuǎn)換:如果目標類型與實際類型不符但可以進行有意義的轉(zhuǎn)換艾帐,SmartCodable 會自動轉(zhuǎn)換值類型乌叶,確保數(shù)據(jù)的正確解析。
-
默認值填充:當某個屬性無法解析時柒爸,SmartCodable 允許自動填充該屬性類型的默認值枉昏,例如將Bool類型的字段默認設為
false
,從而避免了整個解析過程的失敗揍鸟。 - 兼容性和靈活性:SmartCodable 完全兼容標準的Codable協(xié)議兄裂,并在此基礎(chǔ)上提供更多的定制化選項,適應更復雜和多變的數(shù)據(jù)解析需求阳藻。
解析效率
使用這樣的數(shù)組晰奖,數(shù)組的元素項分別設置為: 100個,1000個腥泥,10000個匾南。分別對這五種解析方案進行解析耗時的統(tǒng)計。
[
{
"name": "Anaa Airport",
"iata": "AAA",
"icao": "NTGA",
"coordinates": [-145.51222222222222, -17.348888888888887],
"runways": [
{
"direction": "14L/32R",
"distance": 1502,
"surface": "flexible"
}
]
}
]
理論上SmartCodable的解析效率是低于Codable的蛔外。如果不解析 runways 蛆楞,就是如此。
SmartCodable對于枚舉項的解析更加高效夹厌。所以在本次數(shù)據(jù)對比中豹爹,解析效率最高,甚至高于Codable矛纹。
作者使用的是單元測試中的 measure 函數(shù)進行性能測算臂聋。
struct Smart: SmartCodable {
var name: String?
var iata: String?
var icao: String?
var coordinates: [Double]?
var runways: [Runway]?
struct Runway: SmartCodable {
var direction: String?
var distance: Int?
var surface: Surface?
}
}
func testSmart() {
measure {
guard let objects = [Smart].deserialize(data: data) else {
return
}
XCTAssertEqual(objects.count, count)
}
}
Demo工程中提供了測試用例,請自行下載工程代碼,訪問 Tests.swift 文件孩等。
HandyJSON vs Codable
Codable
和HandyJSON
是兩種常用的方法艾君。
HandyJSON 使用Swift的反射特性來實現(xiàn)數(shù)據(jù)的序列化和反序列化。該機制是非法的肄方,不安全的冰垄, 更多的細節(jié),可以訪問 HandyJSON 的466號issue.
Codable 是Swift標準庫的一部分权她,提供了一種聲明式的方式來進行序列化和反序列化播演,它更為通用。
比較這兩者在性能方面的差異需要考慮不同的數(shù)據(jù)類型和場景伴奥。一般而言写烤,Codable
在以下情況下可能具有比HandyJSON
更低的解析耗時:
-
標準的JSON結(jié)構(gòu): 當解析標準且格式良好的JSON數(shù)據(jù)時,
Codable
通常表現(xiàn)出較好的性能拾徙。這是因為Codable
是Swift標準庫的一部分洲炊,得到了編譯器的優(yōu)化。 -
復雜數(shù)據(jù)模型: 對于包含多層嵌套和復雜數(shù)據(jù)結(jié)構(gòu)的JSON尼啡,
Codable
可能比HandyJSON
更有效暂衡,特別是在類型安全和編譯時檢查方面。 -
類型安全性高的場景:
Codable
提供了更強的類型安全性崖瞭,這有助于在編譯時捕捉錯誤狂巢。在處理嚴格遵循特定模型的數(shù)據(jù)時,這種類型檢查可能帶來性能優(yōu)勢书聚。 -
與Swift特性集成:
Codable
與Swift的其他特性(如類型推斷唧领、泛型等)集成得更緊密,這可能在某些情況下提高解析效率雌续。
然而斩个,這些差異并不是絕對的。HandyJSON
在某些情況下(如處理動態(tài)或非結(jié)構(gòu)化的JSON數(shù)據(jù))可能表現(xiàn)得更好驯杜。性能也會受到JSON數(shù)據(jù)的大小和復雜性受啥、應用的具體實現(xiàn)方式以及運行時環(huán)境等因素的影響。實際應用中鸽心,選擇Codable
或HandyJSON
應基于具體的項目需求和上下文滚局。
如何使用SmartCodable?
使用SmartCodable與使用標準的Codable類似,但你會獲得額外的錯誤處理能力和更加靈活的解析選項顽频。只需將你的數(shù)據(jù)模型遵循SmartCodable協(xié)議藤肢,即可開始享受更加智能的數(shù)據(jù)解析體驗。
cocopods集成
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!
target 'MyApp' do
pod 'SmartCodable'
end
字典類型的解碼
import SmartCodable
struct Model: SmartCodable {
var name: String = ""
}
let dict: [String: String] = ["name": "xiaoming"]
guard let model = Model.deserialize(dict: dict) else { return }
數(shù)組類型解碼
import SmartCodable
struct Model: SmartCodable {
var name: String = ""
}
let dict: [String: String] = ["name": "xiaoming"]
let arr = [dict, dict]
guard let models = [Model].deserialize(array: arr) else { return }
序列化與反序列化
// 字典轉(zhuǎn)模型
guard let xiaoMing = JsonToModel.deserialize(dict: dict) else { return }
// 模型轉(zhuǎn)字典
let studentDict = xiaoMing.toDictionary() ?? [:]
// 模型轉(zhuǎn)json字符串
let json1 = xiaoMing.toJSONString(prettyPrint: true) ?? ""
// json字符串轉(zhuǎn)模型
guard let xiaoMing2 = JsonToModel.deserialize(json: json1) else { return }
解析完成的回調(diào)
class Model: SmartDecodable {
var name: String = ""
var age: Int = 0
var desc: String = ""
required init() { }
// 解析完成的回調(diào)
func didFinishMapping() {
if name.isEmpty {
desc = "\(age)歲的" + "人"
} else {
desc = "\(age)歲的" + name
}
}
}
枚舉的解碼
struct CompatibleEnum: SmartCodable {
init() { }
var enumTest: TestEnum = .a
enum TestEnum: String, SmartCaseDefaultable {
static var defaultCase: TestEnum = .a
case a
case b
case hello = "c"
}
}
讓你的枚舉遵守 SmartCaseDefaultable 協(xié)議冲九,如果枚舉解析失敗谤草,將使用defaultCase作為默認值。
解碼Any
Codable是無法解碼Any類型的莺奸,這樣就意味著模型的屬性類型可以為 Any丑孩,[Any],[String: Any]灭贷。 這對解碼的便利性造成了一定的困擾温学。
官方的解決方案
對非原生類型字段,給它再生成一個struct甚疟,用原生類型來表述屬性就行仗岖。
struct Block: Codable {
let message: String
let index: Int
let transactions: [[String: Any]]
let proof: String
let previous_hash: String
}
改為:
struct Transaction: Codable {
let amount: Int
let recipient: String
let sender: String
}
struct Block: Codable {
let message: String
let index: Int
let transactions: [Transaction]
let proof: String
let previous_hash: String
}
使用泛型
如果情況允許,可以使用泛型來代替览妖。
struct AboutAny<T: Codable>: SmartCodable {
init() { }
var dict1: [String: T] = [:]
var dict2: [String: T] = [:]
}
guard let one = AboutAny<String>.deserialize(dict: dict) else { return }
使用 SmartAny
SmartAny 是SmartCodable 提供了一種方案轧拄。SmartAny是枚舉類型。
public enum SmartAny {
case bool(Bool)
case string(String)
case double(Double)
case cgFloat(CGFloat)
case float(Float)
case int(Int)
case int8(Int8)
case int16(Int16)
case int32(Int32)
case int64(Int64)
case uInt(Int)
case uInt8(UInt8)
case uInt16(UInt16)
case uInt32(UInt32)
case uInt64(UInt64)
case dict([String: SmartAny])
case array([SmartAny])
}
重寫了public init(from decoder: Decoder) throws
方法讽膏,將解析完成的數(shù)據(jù)包裹進SmartAny內(nèi)檩电。
extension SmartAny: Codable {
// 實現(xiàn) Codable
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
}
其他代碼
......
}
}
如果你要獲取原始的數(shù)據(jù),可以使用 peel 方法去殼府树。
extension Dictionary where Key == String, Value == SmartAny {
/// 解析完成會被SmartAny包裹俐末,使用該屬性去殼。
public var peel: [String: Any] {
var temp: [String: Any] = [:]
for (key, value) in self {
temp.updateValue(value.peel, forKey: key)
}
return temp
}
}
extension Array where Element == SmartAny {
/// 解析完成會被SmartAny包裹奄侠,使用該屬性去殼卓箫。
public var peel: [Any] {
var temp: [Any] = []
temp = self.map({
$0.peel
})
return temp
}
}
extension SmartAny {
/// 獲取原本的值
public var peel: Any {
switch self {
case .bool(let v):
return v
case .string(let v):
return v
// 其他代碼
......
}
}
}
更多實現(xiàn)細節(jié)可以訪問SmartAny.swift 文件。
解碼策略 - SmartDecodingOption
public static func deserialize(json: String?, options: [SmartDecodingOption]? = nil) -> Self?
options提供了四種解碼選項垄潮,分別為:
/// 用于在解碼之前自動更改密鑰值的策略
case keyStrategy(KeyDecodingStrategy)
/// 用于解碼 “Date” 值的策略
case dateStrategy(JSONDecoder.DateDecodingStrategy)
/// 用于解碼 “Data” 值的策略
case dataStrategy(JSONDecoder.DataDecodingStrategy)
/// 用于不符合json的浮點值(IEEE 754無窮大和NaN)的策略
case floatStrategy(JSONDecoder.NonConformingFloatDecodingStrategy)
SmartKeyDecodingStrategy
/// key解碼策略
public enum SmartKeyDecodingStrategy {
case useDefaultKeys
case convertFromSnakeCase
case custom([String: String])
}
useDefaultKeys: 使用默認的解析映射方式烹卒。
convertFromSnakeCase: 轉(zhuǎn)駝峰的命名方式。會將本次解析的字段弯洗,全部轉(zhuǎn)成駝峰命名甫题。
custom: 自定義的方式。key是數(shù)據(jù)中的字段名涂召,value是模型中的屬性名坠非。
// 1. CodingKeys 映射
guard let feedOne = FeedOne.deserialize(json: json) else { return }
// 2. 使用keyDecodingStrategy的駝峰命名
guard let feedTwo = FeedTwo.deserialize(json: json, options: [.keyStrategy(.convertFromSnakeCase)]) else { return }
// 3. 使用keyDecodingStrategy的自定義策略
let option: SmartDecodingOption = .keyStrategy(.custom(["nick_name": "name"]))
guard let feedThree = FeedThree.deserialize(json: json, options: [option]) else { return }
Date格式的解碼
let json = """
{
"birth": "2034-12-01 18:00:00"
}
"""
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let option: SmartDecodingOption = .dateStrategy(.formatted(dateFormatter))
guard let model = FeedOne.deserialize(json: json, options: [option]) else { return }
Data類型的解碼
let json = """
{
"address": "aHR0cHM6Ly93d3cucWl4aW4uY29t"
}
"""
let option: SmartDecodingOption = .dataStrategy(.base64)
guard let model = FeedOne.deserialize(json: json, options: [option]) else { return }
if let data = model.address, let url = String(data: data, encoding: .utf8) {
print(url)
// https://www.qixin.com
}
SmartCodable的兼容性
在使用系統(tǒng)的 Codable 解碼的時候,遇到 無鍵果正,值為null炎码, 值類型錯誤 拋出異常導致解析失敗。SmartCodable 底層默認對這三種解析錯誤進行了兼容秋泳。
具體的實現(xiàn)邏輯可以查看 Patcher.swift 文件潦闲。
無鍵 & 值為null
這兩種情況的數(shù)據(jù),我稱之為擺爛數(shù)據(jù)迫皱,這種數(shù)據(jù)無法拯救歉闰。
解析到 無鍵 & 值為null 的時候辖众,SmartCodable會提供該字段類型的默認值進行解析填充(如果是可選類型,提供nil)凹炸。使解析順利進行。
對這兩份數(shù)據(jù)舱痘,解析到 CompatibleTypes 模型中
var json: String {
"""
{
}
"""
}
var json: String {
"""
{
"a": null,
"b": null,
"c": null,
"d": null,
"e": null,
"f": null,
"g": null,
"h": null,
"i": null,
"j": null,
"k": null,
"l": null,
"v": null,
"w": null,
"x": null,
"y": null,
"z": null
}
"""
}
struct CompatibleTypes: SmartDecodable {
var a: String = ""
var b: Bool = false
var c: Date = Date()
var d: Data = Data()
var e: Double = 0.0
var f: Float = 0.0
var g: CGFloat = 0.0
var h: Int = 0
var i: Int8 = 0
var j: Int16 = 0
var k: Int32 = 0
var l: Int64 = 0
var m: UInt = 0
var n: UInt8 = 0
var o: UInt16 = 0
var p: UInt32 = 0
var q: UInt64 = 0
var v: [String] = []
var w: [String: [String: Int]] = [:]
var x: [String: String] = [:]
var y: [String: Int] = [:]
var z: CompatibleItem = CompatibleItem()
}
class CompatibleItem: SmartDecodable {
var name: String = ""
var age: Int = 0
required init() { }
}
解析完成后塌碌,將使用該屬性對應的數(shù)據(jù)類型的默認值進行填充。
guard let person = CompatibleTypes.deserialize(json: json) else { return }
/**
"屬性:a 的類型是 String, 其值為 "
"屬性:b 的類型是 Bool搂漠, 其值為 false"
"屬性:c 的類型是 Date, 其值為 2001-01-01 00:00:00 +0000"
"屬性:d 的類型是 Data, 其值為 0 bytes"
"屬性:e 的類型是 Double拣度, 其值為 0.0"
"屬性:f 的類型是 Float奸晴, 其值為 0.0"
"屬性:g 的類型是 CGFloat逮光, 其值為 0.0"
"屬性:h 的類型是 Int嗡综, 其值為 0"
"屬性:i 的類型是 Int8碑幅, 其值為 0"
"屬性:j 的類型是 Int16, 其值為 0"
"屬性:k 的類型是 Int32裹赴, 其值為 0"
"屬性:l 的類型是 Int64雷猪, 其值為 0"
"屬性:m 的類型是 UInt, 其值為 0"
"屬性:n 的類型是 UInt8验夯, 其值為 0"
"屬性:o 的類型是 UInt16共屈, 其值為 0"
"屬性:p 的類型是 UInt32, 其值為 0"
"屬性:q 的類型是 UInt64泼掠, 其值為 0"
"屬性:v 的類型是 Array<String>括改, 其值為 []"
"屬性:w 的類型是 Dictionary<String, Dictionary<String, Int>>, 其值為 [:]"
"屬性:x 的類型是 Dictionary<String, String>, 其值為 [:]"
"屬性:y 的類型是 Dictionary<String, Int>对粪, 其值為 [:]"
"屬性:z 的類型是 CompatibleItem右冻, 其值為 CompatibleItem(name: \"\", age: 0)"
*/
值類型錯誤
這種情況的數(shù)據(jù)著拭,我稱之為可拯救的數(shù)據(jù)。例如: Model中定義的Bool類型,數(shù)據(jù)中返回的是Int類型的0或1爱榔,String類型的True/true/Yes/No等版姑。
解析到 值類型錯誤 的時候,SmartCodable會嘗試對數(shù)據(jù)值進行類型轉(zhuǎn)換宪肖,如果轉(zhuǎn)換成功健爬,將使用該值娜遵。如果轉(zhuǎn)換失敗,將使用該屬性對應的數(shù)據(jù)類型的默認值進行填充壤短。
Bool類型的轉(zhuǎn)換
/// 兼容Bool類型的值设拟,Model中定義為Bool類型,但是數(shù)據(jù)中是String久脯,Int的情況纳胧。
static func compatibleBoolType(value: Any) -> Bool? {
switch value {
case let intValue as Int:
if intValue == 1 {
return true
} else if intValue == 0 {
return false
} else {
return nil
}
case let stringValue as String:
switch stringValue {
case "1", "YES", "Yes", "yes", "TRUE", "True", "true":
return true
case "0", "NO", "No", "no", "FALSE", "False", "false":
return false
default:
return nil
}
default:
return nil
}
}
String類型的轉(zhuǎn)換
/// 兼容String類型的值
static func compatibleStringType(value: Any) -> String? {
switch value {
case let intValue as Int:
let string = String(intValue)
return string
case let floatValue as Float:
let string = String(floatValue)
return string
case let doubleValue as Double:
let string = String(doubleValue)
return string
default:
return nil
}
}
其他更多類型
請查看 TypePatcher.swift 了解更多。
調(diào)試日志
SmartCodable鼓勵從根本上解決解析中的問題桶现,即:不需要用到SmartCodable的兼容邏輯躲雅。 如果出現(xiàn)解析兼容的情況鼎姊,修改Model中屬性的定義骡和,或要求數(shù)據(jù)方進行修正。為了更方便的定位問題相寇,SmartCodable提供了便捷的解析錯誤日志慰于。
調(diào)試日志,將提供輔助信息唤衫,幫助定位問題:
- 錯誤類型: 錯誤的類型信息
- 模型名稱:發(fā)生錯誤的模型名出
- 數(shù)據(jù)節(jié)點:發(fā)生錯誤時婆赠,數(shù)據(jù)的解碼路徑。
- 屬性信息:發(fā)生錯誤的字段名佳励。
- 錯誤原因: 錯誤的具體原因休里。
================ [SmartLog Error] ================
錯誤類型: '找不到鍵的錯誤'
模型名稱:Array<Class>
數(shù)據(jù)節(jié)點:Index 0 → students → Index 0
屬性信息:(名稱)more
錯誤原因: No value associated with key CodingKeys(stringValue: "more", intValue: nil) ("more").
==================================================
================ [SmartLog Error] ================
錯誤類型: '值類型不匹配的錯誤'
模型名稱:DecodeErrorPrint
數(shù)據(jù)節(jié)點:a
屬性信息:(類型)Bool (名稱)a
錯誤原因: Expected to decode Bool but found a string/data instead.
==================================================
================ [SmartLog Error] ================
錯誤類型: '找不到值的錯誤'
模型名稱:DecodeErrorPrint
數(shù)據(jù)節(jié)點:c
屬性信息:(類型)Bool (名稱)c
錯誤原因: c 在json中對應的值是null
==================================================
你可以通過SmartConfig 調(diào)整日志的相關(guān)設置。
如何理解數(shù)據(jù)節(jié)點赃承?
右側(cè)的數(shù)據(jù)是數(shù)組類型妙黍。注意標紅的內(nèi)容,由外到里對照查看瞧剖。
Index 0: 數(shù)組的下標為0的元素拭嫁。
sampleFive: 下標為0的元素對應的是字典,即字典key為sampleFive對應的值(是一個數(shù)組)抓于。
Index 1:數(shù)組的下標為1的元素.
sampleOne:字典中key為sampleOne對應的值做粤。
string:字典中key為sring對應的值。
四. SamrtCodable的缺點
1. 可選模型屬性
struct Feed: SmartCodable {
var one: FeedOne?
}
struct FeedOne: SmartCodable {
var name: String = ""
}
如有模型中的屬性是嵌套的模型屬性捉撮,遇到類型不匹配的情況怕品,Codable無法解析拋出異常,這種情況的異常巾遭,SmartCodale將無法兼容肉康。
此時您有兩種選擇:
- 將Feed中one這個屬性設置為非可選的修己。 SmartCodable 將正常工作。
- 將該屬性使用 @SmartOptional 屬性包裝器修飾迎罗。
struct Feed: SmartCodable {
@SmartOptional var one: FeedOne?
}
class FeedOne: SmartCodable {
var name: String = ""
required init() { }
}
這是一個不得已的實現(xiàn)方案:
為了做解碼失敗的兼容睬愤,我們重寫了 KeyedEncodingContainer 的 decode 和 decodeIfPresent 方法。
需要注意的是:decodeIfPresent底層的實現(xiàn)仍是使用的 decode纹安。
// 系統(tǒng)Codable源碼實現(xiàn)
public extension KeyedDecodingContainerProtocol {
@_inlineable // FIXME(sil-serialize-all)
public func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? {
guard try self.contains(key) && !self.decodeNil(forKey: key) else { return nil }
return try self.decode(Bool.self, forKey: key)
}
}
KeyedEncodingContainer容器是用結(jié)構(gòu)體實現(xiàn)的尤辱。 重寫了結(jié)構(gòu)體的方法之后,沒辦法再調(diào)用父方法厢岂。
- 這種情況下光督,如果再重寫
public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T?
方法,就會導致方法的循環(huán)調(diào)用塔粒。 - 使用SmartOptional屬性包裝器修飾可選屬性结借,被修飾后會產(chǎn)生一個新的類型,對此類型解碼就不會走decodeIfPresent卒茬,而是會走decode方法船老。
使用SmartOptional的三個限制條件
必須遵循SmartDecodable協(xié)議
-
必須是可選屬性
如果不是可選屬性,就沒必要使用SmartOptional圃酵。
-
必須是class類型
如果模型是Struct柳畔,是值類型。在執(zhí)行 didFinishMapping 的時候郭赐,無法初始化被屬性包裝器修飾的屬性薪韩,進而無法有效的執(zhí)行解碼完成之后的值修改。
如果你有更好的方案捌锭,可以提issue俘陷。
2. 模型中設置的默認值無效
Codable在進行解碼的時候,是無法知道這個屬性的观谦。所以在decode的時候拉盾,如果解析失敗,使用默認值進行填充時坎匿,拿不到這個默認值盾剩。再處理解碼兼容時,只能自己生成一個對應類型的默認值填充替蔬。
如果你有更好的方案告私,可以提issue。
進一步了解
我們提供了詳細的示例工程承桥,可以下載工程代碼查看驻粟。
加入我們
SmartCodable 是一個開源項目,我們歡迎所有對提高數(shù)據(jù)解析性能和健壯性感興趣的開發(fā)者加入。無論是使用反饋蜀撑、功能建議還是代碼貢獻挤巡,你的參與都將極大地推動 SmartCodable 項目的發(fā)展。