ios中字典轉模型(json轉模型)原理及應用

爲了代碼可讀性以及開發效率,咱們每每會將數據抽象爲數據模型,在開發過程當中操做數據模型而不是數據自己。php

在開發過程當中,咱們須要將key-value結構的數據,也就是字典,轉化爲數據模型。也就是字典轉模型啦。git

字典轉模型主要應用在兩個場景。網絡請求(json解析爲模型、模型轉字典做爲請求參數),模型的數據持久化存取。github

下面咱們來分別探討一下,OC跟swift中幾種主流的字典轉模型方式。json

1 swift中字典轉模型的方式

1.1 Codable

Codable是swift4開始引入的類型,它包含兩個協議Decodable Encodableswift

public typealias Codable = Decodable & Encodable

public protocol Encodable {
    func encode(to encoder: Encoder) throws
}

public protocol Decodable {
    init(from decoder: Decoder) throws
}
複製代碼

使用Codable能夠很方便的將數據解碼爲數據模型,將數據模型編碼成數據。緩存

讓模型聽從 Codable,一切將都變得很簡單

EncodableDecodable都有默認的實現。當咱們讓模型聽從Codable後,就能夠但是編解碼了。安全

class User : Codable {
	var name : String?
	...
}
複製代碼

json轉模型bash

let model = try? JSONDecoder().decode(User.self, from: jsonData)
複製代碼

模型轉json網絡

let tdata = try? JSONEncoder().encode(model)
複製代碼

而一般狀況下,咱們要處理的是字典轉模型,將json解析步驟交給網絡請求庫。而Codable在實現json轉模型過程當中,也是先解析爲字典再進行模型轉化的。數據結構

因此一般狀況咱們的使用姿式是這樣的:

// 字典轉模型
func decode<T>(json:Any)->T? where T:Decodable{
    do {
        let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
        let model = try JSONDecoder().decode(T.self, from: data)
        return model
    } catch let err {
        print(err)
        return nil
    }
}

// 模型轉字典
func encode<T>(model:T) ->Any? where T:Encodable {
    do {
        let tdata = try JSONEncoder().encode(model)
        let tdict  = try JSONSerialization.jsonObject(with: tdata, options: JSONSerialization.ReadingOptions.allowFragments)
        return tdict
    } catch let error {
        print(error)
        return nil
    }
}
複製代碼
如何讓咱們的模型順利聽從 Codable

當咱們的模型申明遵循Codable協議時,常常會看到碰到does not conform to protocol的報錯,那麼如何才能讓模型順利準從Codable

已經聽從Codable的系統庫中的類型:
  • 全部的基礎類型,Int,Double, String...
  • swift Foundation中的大部分類型:URL、Data、Date、IndexPath等,不包含OC類型
  • 集合類型:Array,Dictionary<Key, Value>, Set等。須要注意的是: 其全部包含的子類型都要聽從Codable
在聽從Codable時,咱們須要注意:
  • 全部屬性的類型都必須聽從Codable。除了給咱們咱們自定義的子類都加上Codable申明外,其餘類型必須是上述Codable類型之一
  • enum類型必須實現RawRepresentable,也就是須要定義原始值類型。並且原始值類型必須也遵循Codable
若是須要包含未遵循Codable屬性如何處理

在項目當中,咱們有時候須要在模型裏申明一些非Codable屬性,譬如CLLocation、AVAsset等。接下來,咱們來探討一下解決這類需求有那些可行方案

  • 計算屬性不會形象對Codable協議的實現。對於任何計算屬性咱們不用關心它是否Codable
  • 經過extension爲非Codable類型添加遵循Codable的申明
    • 首先在extension中申明繼承Encodable協議,是支持的,除了class類型必須聲明爲finalDecodable不支持)外。

    • 可是它有個苛刻的條件,extension必須與類型申明在同一文件。

Implementation of 'Decodable' cannot be automatically synthesized in an extension in a different file to the type

- 這種方式顯然是不可行的,除非咱們能夠修改相應類型的源碼
複製代碼
  • 實現CodingKeys,而且不包含非Codable屬性。

    • 若是CodingKeys中不包含非Codable類型的case,是能夠經過編譯的:

      struct Destination : Codable {
          var location : CLLocationCoordinate2D?
          var city : String?
          enum CodingKeys : String,CodingKey {
              case city
          }
      }
      複製代碼
  • 本身實現EncodableDecodable並作相應轉化

    • 經驗證,只要手動實現了EncodableDecodable協議中的方法,任何非Codable屬性的申明都不會報錯。而咱們須要作的是完成特定的數據結構與非Codable的類型屬性的相互轉化
什麼狀況下會轉換失敗
  • 類型不一致:當屬性聲明的類型與字典中相應字段的類型不一致時,會拋出異常
  • 當一個非空屬性,在字典中找不到對應的key,或者value爲空時,會拋出異常
key的特殊處理
  • 若是不但願全部key參與編解碼,或者字典中key和屬性名不一樣。

能夠申明一個CodingKeys枚舉。

enum Codingkeys : String,CodingKey {
    case id,name,age
    case userId = "user_id"
}
複製代碼

這樣作了以後,在轉化過程當中,會只處理全部已申明的case,而且根據原始值肯定key與屬性的對應關係。

若是繼承Codable,系統默認會自動合成一個繼承CodingKey協議的枚舉類型CodingKeys,而且包含全部申明的屬性值(不包含static變量),而且stringValue與屬性名稱一致。

CodingKeys的默認實現是private的,只能在類內部使用

'CodingKeys' is inaccessible due to 'private' protection level

DecodableEncodable的默認實現都是基於CodingKeys。若是咱們本身將其申明瞭,系統就會使用咱們申明的CodingKeys而再也不自動生成。

值得注意的是:如過CodingKeys中包含與屬性中沒有的case,會報錯 does not conform to protocol

  • 僅僅是命名風格差別

直接設置經過JSONDecoder的設置就能夠完成

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
複製代碼

它能夠講全部帶下劃線分割符的key,轉化爲駝峯命名法

對value的特殊處理
一些能夠自動轉化的value類型
  • enum類型:能夠自動將rawValue值轉化爲enum類型值
  • URL類型:能夠自動將字符串轉爲URL
  • Date日期類型

默認狀況下能夠將Double類型,也就是2001年1月1日到如今的時間戳(timeIntervalSinceReferenceDate),轉化爲Date類型。 咱們能夠經過JSONDecoderdateDecodingStrategy屬性,來制定 Date 類型的解析策略

public enum DateDecodingStrategy {
    /// default strategy.
    case deferredToDate
    
    case secondsSince1970
    case millisecondsSince1970
    case formatted(DateFormatter)
    case custom((Decoder) throws -> Date)
    
    ///  ISO-8601-formatted string 
    case iso8601
}
複製代碼
手動實現協議方法

作了上面這些已經能解決大部分業務場景,可是咱們須要在編解碼過程當中對屬性值進行轉換,或者作多級映射,咱們須要實現Encodable``Decodable兩個協議方法,並對屬性值作適當處理與轉化

struct Destination:Codable {
    var  location : CLLocationCoordinate2D

    private enum CodingKeys:String,CodingKey{
        case latitude
        case longitude
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy:CodingKeys.self)
        try container.encode(location.latitude,forKey:.latitude)
        try container.encode(location.longitude,forKey:.longitude)
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let latitude = try container.decode(CLLocationDegrees.self,forKey:.latitude)
        let longitude = try container.decode(CLLocationDegrees.self,forKey:.longitude)
        self.location = CLLocationCoordinate2D(latitude:latitude,longitude:longitude)
    }
    
}
複製代碼
1.2 ObjectMapper

ObjectMapper是用swift編寫的json轉模型類庫。它主要依賴Mappable全部實現了這個協議的類型,均可以輕鬆實現json與模型之間的轉化

使用方式

首先在類中實現Mappable

class UserMap : Mappable {
    var name : String?
    var age : Int?
    var gender : Gender?
    var body : Body?
    
    required init?(map: Map) {}
    
    // Mappable
    func mapping(map: Map) {
        name    		<- map["name"]
        age         <- map["age"]
        gender      <- map["gender"]
        body        <- map["body"]
    }
    
}
複製代碼

字典轉模型

let u = UserMap(JSON:dict)
複製代碼

模型轉字典

let json = u?.toJSON()
複製代碼
優缺點
  • 若是是swift項目,不用擔憂橋接問題,代碼看起來更swifty。
  • 字典屬性的增刪改,對轉化過程影響也很小。當字典中類型與屬性類型不一致時,屬性將賦予nil值,其餘屬性不受影響
  • 缺點也很明顯,實現func mapping(map: Map)方法,肯定屬性與key映射關係,這一步驟會尤其繁瑣,尤爲是在屬性值多時
自動化生成代碼

固然,有人將這個繁瑣的過程自動化了

  • ObjectMapper-Plugin一個讓當前類型代碼自動添加Mappable實現的插件

  • json4swift一個自動生成swift模型代碼的網站。它是一個很好用的json轉數據模型代碼的工具。支持Codable、ObjectMapper、Classic Swift Key/Value Dictionary(經過硬編碼方式,根據key-value經過屬性的getter、setter方法賦值)

1.3 HandyJSON

與Codable用法類似,讓模型遵循HandyJSON就能夠進行字典轉模型、json轉模型了。

原理上,主要使用Reflection,依賴於從Swift Runtime源碼中推斷的內存規則

這是一個阿里的項目,感興趣的同窗能夠移步他的github

2 OC中字典轉模型的方式
2.1 KVC

KVC全稱是Key Value Coding,定義在NSKeyValueCoding.h文件中,是一個非正式協議。KVC提供了一種間接訪問其屬性方法或成員變量的機制,能夠經過字符串來訪問對應的屬性方法或成員變量。

其核心方法是:

- (void)setValue:(nullable id)value forKey:(NSString *)key;

- (nullable id)valueForKey:(NSString *)key;
複製代碼

在字典轉模型的方法,你們都很熟悉了:

Person *person = [[Person alloc] init];
[person setValuesForKeysWithDictionary:dic];
複製代碼

它等同於

Person *p0 = [[Person alloc] init];
for (NSString *key in dic) {
    [p0 setValue:dic[key] forKey:key];
}
複製代碼

惟一不一樣的是若是字典或json中存在null時,用setValue:forKey:等到的是NSNull的屬性值,而setValuesForKeysWithDictionary:獲得的是相應屬性類型的nil值

模型轉字典,能夠用:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
複製代碼

在使用時,須要注意的是:

  • 字典中的key必須與對象屬性名一一對應
  • 爲了不字典中存在多餘的鍵值而致使崩潰,每每會在模型類的實現中加上:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
複製代碼
  • 對於值類型屬性,通常咱們直接聲明爲對應的值類型,如int、double、BOOL、CGRect等,而沒必要聲明爲NSNumber或NSValue。而無論用那種聲明方式都是能夠正確轉換的。
嵌套類型

對於嵌套類型kvc是不支持直接轉化的,可是咱們可用經過重寫相應屬性的setter方法來實現

- (void)setBody:(Body *)body
{
    if (![body isKindOfClass:[Body class]] && [body isKindOfClass:[NSDictionary class]]) {
        _body = [[Body alloc] init];
        [_body setValuesForKeysWithDictionary:(NSDictionary *)body];
    }else{
        _body = body;
    }
}
複製代碼

若是value要作必定轉化時,也能夠用相似方法

key值轉化

當字典中key值與屬性名存在差別時,能夠經過重寫setValue:forUndefinedKey實現

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        self.ID = value;
    }
}
複製代碼
在swift中使用

若是須要在swift中使用,首先模型類必須繼承自NSObject,全部須要的屬性必須加上@objc修飾符

2.2 MJExtension
2.2.1 使用方法

MJExtension是OC中字典轉模型最經常使用的第三方庫。他的使用很簡單。

// 字典轉模型
User *user = [User mj_objectWithKeyValues:dict];

// 模型轉字典
NSDictionary *userDict = user.mj_keyValues;
複製代碼

而相較於直接使用kvc,它還還支持不少獨有的特性

嵌套模型

它支持下述這種嵌套模型。

@interface Body : NSObject
@property (nonatomic,assign)double weight;
@property (nonatomic,assign)double height;
@end

@interface Person : NSObject
@property (nonatomic,strong)NSString *ID;
@property (nonatomic,strong)NSString *userId;
@property (nonatomic,strong)NSString *oldName;
@property (nonatomic,strong)Body *body;
@end

複製代碼
key值轉化:

若是字典與模型的命名風格不一致,或者須要多級映射。須要在模型類的實現中添加下列方法的實現

+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
    return @{
             @"ID" : @"id",
             @"userId" : @"user_id",
             @"oldName" : @"name.oldName"
             };
}
複製代碼

值得注意的是,除了這個字典中的key作相應轉化以外,其餘屬性和key是不受任何影響的。

過濾規則
+ (NSArray *)mj_ignoredPropertyNames
{
    return @[@"selected"];
}
複製代碼
2.2.1 原理

MJExtension主要運用了KVC和OC反射機制

字典轉模型

字典轉模型時,其核心是KVC,主要運用:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
複製代碼

在爲每個屬性賦值以前,它使用runtime函數,運用oc反射機制,獲取全部屬性的屬性名及類型。在對全部屬性名進行白名單和黑名單的過濾,經過對屬性類型推斷,對鍵值進行相應轉換,保證鍵值的安全有效,也對嵌套類型作相應處理。還包含一些緩存策略。

核心代碼可概括以下:

- (id)objectWithKeyValues:(NSDictionary *)keyValues type:(Class)type
{
    id obj = [[type alloc] init];
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList([Person class],&outCount);
    for (int i = 0; i < outCount;i++) {
        objc_property_t property = properties[i];
        // 獲取屬性名
        NSString *name = @(property_getName(property));
        // 獲取成員類型
        NSString *attrs = @(property_getAttributes(property));
        
        NSString *code = [self codeWithAttributes:attrs];
        // 類型轉換爲class
        Class propertyClass = [self classWithCode:code];
        
        id value = keyValues[name];
        
        if (!value || value == [NSNull null]) continue;
        
        if (![self isFoundation:propertyClass] && propertyClass) {
            // 模型屬性
            value = [self objectWithKeyValues:value type:propertyClass];
        } else {
            value = [self convertValue:value propertyClass:propertyClass code:code];
        }
        // kvc設置屬性值
        [obj setValue:value forKey:name];
    }
    return obj;
}
複製代碼
模型轉字典

主要流程是經過反射獲取屬性名稱(key)的列表,再經過valueForKey:獲取屬性值(value),而後對key進行過濾轉換,value進行轉換,最後把全部鍵值對放入字典中獲得結果。

在swift中使用

若是須要在swift中使用,首先模型類必須繼承自NSObject,全部須要的屬性必須加上@objc修飾符

避免使用Bool類型(官方文檔的提示。而Swift五、Xcode10中實測,除了轉成字典時bool變爲0和1外,無其餘異常)

相關文章
相關標籤/搜索