揭祕 YYModel 的魔法(下)

Lision發表於2019-03-04

前言

在上文《揭祕 YYModel 的魔法(上)》 中主要剖析了 YYModel 的原始碼結構,並且分享了 YYClassInfo 與 NSObject+YYModel 內部有趣的實現細節。

緊接上篇,本文將解讀 YYModel 關於 JSON 模型轉換的原始碼,旨在揭祕 JSON 模型自動轉換魔法。

索引

  • JSON 與 Model 相互轉換
  • 總結

JSON 與 Model 相互轉換

JSON(JavaScript Object Notation) 是一種輕量級的資料交換格式,它易於人們閱讀和編寫,同時也易於機器解析和生成。它是基於 JavaScript Programming Language, Standard ECMA-262 3rd Edition – December 1999 的一個子集。JSON 採用完全獨立於語言的文字格式,但是也使用了類似於 C 語言家族的習慣(包括C, C++, C#, Java, JavaScript, Perl, Python等)。這些特性使 JSON 成為理想的資料交換語言,點選 這裡 瞭解更多關於 JSON 的資訊。

Model 是 物件導向程式設計(Object Oriented Programming,簡稱 OOP)程式設計思想中的物件,OOP 把物件作為程式的基本單元,一個物件包含了資料和運算元據的函式。一般我們會根據業務需求來建立物件,在一些設計模式中(如 MVC 等)物件一般作為模型(Model),即物件建模。

JSON 與 Model 相互轉換按轉換方向分為兩種:

  • JSON to Model
  • Model to JSON
揭祕 YYModel 的魔法(下)

JSON to Model

我們從 YYModel 的介面開始解讀。

+ (instancetype)yy_modelWithJSON:(id)json {
    // 將 json 轉為字典 dic
    NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
    // 再通過 dic 得到 model 並返回
    return [self yy_modelWithDictionary:dic];
}
複製程式碼

上面介面把 JSON 轉 Model 很簡單的分為了兩個子任務:

  • JSON to NSDictionary
  • NSDictionary to Model
揭祕 YYModel 的魔法(下)

JSON to NSDictionary

我們先看一下 _yy_dictionaryWithJSON 是怎麼將 json 轉為 NSDictionary 的。

+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json {
    // 入參判空
    if (!json || json == (id)kCFNull) return nil;
    
    NSDictionary *dic = nil;
    NSData *jsonData = nil;
    // 根據 json 的型別對應操作
    if ([json isKindOfClass:[NSDictionary class]]) {
        // 如果是 NSDictionary 類則直接賦值
        dic = json;
    } else if ([json isKindOfClass:[NSString class]]) {
        // 如果是 NSString 類則用 UTF-8 編碼轉 NSData
        jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
    } else if ([json isKindOfClass:[NSData class]]) {
        // 如果是 NSData 則直接賦值給 jsonData
        jsonData = json;
    }
    
    // jsonData 不為 nil,則表示上面的 2、3 情況中的一種
    if (jsonData) {
        // 利用 NSJSONSerialization 方法將 jsonData 轉為 dic
        dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
        // 判斷轉換結果 
        if (![dic isKindOfClass:[NSDictionary class]]) dic = nil;
    }
    
    return dic;
}
複製程式碼

這個函式主要是根據入參的型別判斷如何將其轉為 NSDictionary 型別並返回。

其中 kCFNull 是 CoreFoundation 中 CFNull 的單例物件。如同 Foundation 框架中的 NSNull 一樣,CFNull 是用來表示集合物件中的空值(不允許為 NULL)。CFNull 物件既不允許被建立也不允許被銷燬,而是通過定義一個 CFNull 常量,即 kCFNull,在需要空值時使用。

官方文件:
The CFNull opaque type defines a unique object used to represent null values in collection objects (which don’t allow NULL values). CFNull objects are neither created nor destroyed. Instead, a single CFNull constant object—kCFNull—is defined and is used wherever a null value is needed.

NSJSONSerialization 是用於將 JSON 和等效的 Foundation 物件之間相互轉換的物件。它在 iOS 7 以及 macOS 10.9(包含 iOS 7 和 macOS 10.9)之後是執行緒安全的。

程式碼中將 NSString 轉為 NSData 用到了 NSUTF8StringEncoding,其中編碼型別必須屬於 JSON 規範中列出的 5 種支援的編碼型別:

  • UTF-8
  • UTF-16LE
  • UTF-16BE
  • UTF-32LE
  • UTF-32BE

而用於解析的最高效的編碼是 UTF-8 編碼,所以作者這裡使用 NSUTF8StringEncoding。

官方註釋:
The data must be in one of the 5 supported encodings listed in the JSON specification: UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE. The data may or may not have a BOM. The most efficient encoding to use for parsing is UTF-8, so if you have a choice in encoding the data passed to this method, use UTF-8.

NSDictionary to Model

現在我們要從 yy_modelWithJSON 介面中探究 yy_modelWithDictionary 是如何將 NSDictionary 轉為 Model 的。

敲黑板!做好準備,這一小節介紹的程式碼是 YYModel 的精華哦~。

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    // 入參校驗
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    
    // 使用當前類生成一個 _YYModelMeta 模型元類
    Class cls = [self class];
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    // 這裡 _hasCustomClassFromDictionary 用於標識是否需要自定義返回類
    // 屬於模型轉換附加功能,可以不用投入太多關注
    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    
    // 呼叫 yy_modelSetWithDictionary 為新建的類例項 one 賦值,賦值成功則返回 one
    NSObject *one = [cls new];
    // 所以這個函式中我們應該把注意力集中在 yy_modelSetWithDictionary
    if ([one yy_modelSetWithDictionary:dictionary]) return one;
    
    return nil;
}
複製程式碼

程式碼中根據 _hasCustomClassFromDictionary 標識判斷是否需要自定義返回模型的型別。這段程式碼屬於 YYModel 的附加功能,為了不使大家分心,這裡僅做簡單介紹。

如果我們要在 JSON 轉 Model 的過程中根據情況建立不同型別的例項,則可以在 Model 中實現介面:

+ (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;
複製程式碼

來滿足需求。當模型元初始化時會檢測當前模型類是否可以響應上面的介面,如果可以響應則會把 _hasCustomClassFromDictionary 標識為 YES,所以上面才會出現這些程式碼:

if (modelMeta->_hasCustomClassFromDictionary) {
    cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}
複製程式碼

嘛~ 我覺得這些附加的東西在閱讀原始碼時很大程度上會分散我們的注意力,這次先詳細的講解一下,以後遇到類似的程式碼我們會略過,內部的實現大都與上述案例原理相同,感興趣的同學可以自己研究哈。

我們應該把注意力集中在 yy_modelSetWithDictionary 上,這個函式(其實也是 NSObject+YYModel 暴露的介面)是根據字典初始化模型的實現方法。它的程式碼比較長,如果不想看可以跳過,在後面有解釋。

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    // 入參校驗
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    
    // 根據自身類生成 _YYModelMeta 模型元類
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    // 如果模型元類鍵值對映數量為 0 則 return NO,表示構建失敗
    if (modelMeta->_keyMappedCount == 0) return NO;
    
    // 忽略,該標識對應 modelCustomWillTransformFromDictionary 介面
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        // 該介面類似 modelCustomTransformFromDictionary 介面,不過是在模型轉換之前呼叫的
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }
    
    // 初始化模型設定上下文 ModelSetContext
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic);
    
    // 判斷模型元鍵值對映數量與 JSON 所得字典的數量關係
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        // 一般情況下他們的數量相等
        // 特殊情況比如有的屬性元會對映字典中的多個 key
        
        // 為字典中的每個鍵值對呼叫 ModelSetWithDictionaryFunction
        // 這句話是核心程式碼,一般情況下就是靠 ModelSetWithDictionaryFunction 通過字典設定模型
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        // 判斷模型中是否存在對映 keyPath 的屬性元
        if (modelMeta->_keyPathPropertyMetas) {
            // 為每個對映 keyPath 的屬性元執行 ModelSetWithPropertyMetaArrayFunction
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        // 判斷模型中是否存在對映多個 key 的屬性元
        if (modelMeta->_multiKeysPropertyMetas) {
            // 為每個對映多個 key 的屬性元執行 ModelSetWithPropertyMetaArrayFunction
            CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
    } else { // 模型元鍵值對映數量少,則認為不存在對映多個 key 的屬性元
        // 直接為每個 modelMeta 屬性元執行 ModelSetWithPropertyMetaArrayFunction
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    // 忽略,該標識對應介面 modelCustomTransformFromDictionary
    if (modelMeta->_hasCustomTransformFromDictionary) {
        // 該介面用於當預設 JSON 轉 Model 不適合模型物件時做額外的邏輯處理
        // 我們也可以用這個介面來驗證模型轉換的結果
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    
    return YES;
}
複製程式碼

程式碼已經註明必要中文註釋,關於兩處自定義擴充套件介面我們不再多說,由於程式碼比較長我們先來梳理一下 yy_modelSetWithDictionary 主要做了哪些事?

  • 入參校驗
  • 初始化模型元以及對映表校驗
  • 初始化模型設定上下文 ModelSetContext
  • 為字典中的每個鍵值對呼叫 ModelSetWithDictionaryFunction
  • 檢驗轉換結果

模型設定上下文 ModelSetContext 其實就是一個包含模型元,模型例項以及待轉換字典的結構體。

typedef struct {
    void *modelMeta;  ///< 模型元
    void *model;      ///< 模型例項,指向輸出的模型
    void *dictionary; ///< 待轉換字典
} ModelSetContext;
複製程式碼

大家肯定都注意到了 ModelSetWithDictionaryFunction 函式,不論走哪條邏輯分支,最後都是呼叫這個函式把字典的 key(keypath)對應的 value 取出並賦值給 Model 的,那麼我們就來看看這個函式的實現。

// 字典鍵值對建模
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
    // 拿到入參上下文
    ModelSetContext *context = _context;
    // 取出上下文中模型元
    __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
    // 根據入參 _key 從模型元中取出對映表對應的屬性元
    __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
    // 拿到待賦值模型
    __unsafe_unretained id model = (__bridge id)(context->model);
    // 遍歷 propertyMeta,直到 propertyMeta->_next == nil
    while (propertyMeta) {
        // 當前遍歷的 propertyMeta 有 setter 方法,則呼叫 ModelSetValueForProperty 賦值
        if (propertyMeta->_setter) {
            // 核心方法,拎出來講
            ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
        }
        propertyMeta = propertyMeta->_next;
    };
}
複製程式碼

ModelSetWithDictionaryFunction 函式的實現邏輯就是先通過模型設定上下文拿到帶賦值模型,之後遍歷當前的屬性元(直到 propertyMeta->_next == nil),找到 setter 不為空的屬性元通過 ModelSetValueForProperty 方法賦值。

ModelSetValueForProperty 函式是為模型中的屬性賦值的實現方法,也是整個 YYModel 的核心程式碼。別緊張,這個函式寫得很友好的,也就 300 多行而已 ?(無關緊要的內容我會盡量忽略掉),不過忽略的太多會影響程式碼閱讀的連續性,如果嫌長可以不看,文章後面會總結一下這個函式的實現邏輯。

static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta) {
    // 如果屬性是一個 CNumber,即輸入 int、uint……
    if (meta->_isCNumber) {
        // 轉為 NSNumber 之後賦值
        NSNumber *num = YYNSNumberCreateFromID(value);
        // 這裡 ModelSetNumberToProperty 封裝了給屬性元賦值 NSNumber 的操作
        ModelSetNumberToProperty(model, num, meta);
        if (num) [num class]; // hold the number
    } else if (meta->_nsType) {
        // 如果屬性屬於 nsType,即 NSString、NSNumber……
        if (value == (id)kCFNull) { // 為空,則賦值 nil(通過屬性元 _setter 方法使用 objc_msgSend 將 nil 賦值)
            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
        } else { // 不為空
            switch (meta->_nsType) {
                // NSString 或 NSMutableString
                case YYEncodingTypeNSString:
                case YYEncodingTypeNSMutableString: {
                    // 處理可能的 value 型別:NSString,NSNumber,NSData,NSURL,NSAttributedString
                    // 對應的分支就是把 value 轉為 NSString 或者 NSMutableString,之後呼叫 setter 賦值
                    ...
                } break;
                
                // NSValue,NSNumber 或 NSDecimalNumber
                case YYEncodingTypeNSValue:
                case YYEncodingTypeNSNumber:
                case YYEncodingTypeNSDecimalNumber: {
                    // 對屬性元的型別分情況賦值(中間可能會涉及到型別之間的轉換)
                    ...
                } break;
                    
                // NSData 或 NSMutableData
                case YYEncodingTypeNSData:
                case YYEncodingTypeNSMutableData: {
                    // 對屬性元的型別分情況賦值(中間可能會涉及到型別之間的轉換)
                    ...
                } break;
                    
                // NSDate
                case YYEncodingTypeNSDate: {
                    // 考慮可能的 value 型別:NSDate 或 NSString
                    // 轉換為 NSDate 之後賦值
                    ...
                } break;
                    
                // NSURL
                case YYEncodingTypeNSURL: {
                    // 考慮可能的 value 型別:NSURL 或 NSString
                    // 轉換為 NSDate 之後賦值(這裡對 NSString 的長度判斷是否賦值 nil)
                    ...
                } break;
                    
                // NSArray 或 NSMutableArray
                case YYEncodingTypeNSArray:
                case YYEncodingTypeNSMutableArray: {
                    // 對屬性元的泛型判斷
                    if (meta->_genericCls) { // 如果存在泛型
                        NSArray *valueArr = nil;
                        // value 所屬 NSArray 則直接賦值,如果所屬 NSSet 類則轉為 NSArray
                        if ([value isKindOfClass:[NSArray class]]) valueArr = value;
                        else if ([value isKindOfClass:[NSSet class]]) valueArr = ((NSSet *)value).allObjects;
                        
                        // 遍歷剛才通過 value 轉換來的 valueArr
                        if (valueArr) {
                            NSMutableArray *objectArr = [NSMutableArray new];
                            for (id one in valueArr) {
                                // 遇到 valueArr 中的元素屬於泛型類,直接加入 objectArr
                                if ([one isKindOfClass:meta->_genericCls]) {
                                    [objectArr addObject:one];
                                } else if ([one isKindOfClass:[NSDictionary class]]) {
                                    // 遇到 valueArr 中的元素是字典類,
                                    Class cls = meta->_genericCls;
                                    // 忽略
                                    if (meta->_hasCustomClassFromDictionary) {
                                        cls = [cls modelCustomClassForDictionary:one];
                                        if (!cls) cls = meta->_genericCls; // for xcode code coverage
                                    }
                                    // 還記得我們直接的起點 yy_modelSetWithDictionary,將字典轉模型
                                    // 我覺得這應該算是一個間接遞迴呼叫
                                    // 如果設計出的模型是無限遞迴(從前有座山,山上有座廟的故事),那麼肯定會慢
                                    NSObject *newOne = [cls new];
                                    [newOne yy_modelSetWithDictionary:one];
                                    // 轉化成功機也加入 objectArr
                                    if (newOne) [objectArr addObject:newOne];
                                }
                            }
                            // 最後將得到的 objectArr 賦值給屬性
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, objectArr);
                        }
                    } else {
                        // 沒有泛型,嘛~ 判斷一下 value 的可能所屬型別 NSArray 或 NSSet
                        // 轉換賦值(涉及 mutable)
                        ...
                    }
                } break;
                
                // NSDictionary 或 NSMutableDictionary
                case YYEncodingTypeNSDictionary:
                case YYEncodingTypeNSMutableDictionary: {
                    // 跟上面陣列的處理超相似,泛型的間接遞迴以及無泛型的型別轉換(mutable 的處理)
                    ...
                } break;
                    
                // NSSet 或 NSMutableSet
                case YYEncodingTypeNSSet:
                case YYEncodingTypeNSMutableSet: {
                    // 跟上面陣列的處理超相似,泛型的間接遞迴以及無泛型的型別轉換(mutable 的處理)
                    ...
                }
                
                default: break;
            }
        }
    } else { // 屬性元不屬於 CNumber 和 nsType 
        BOOL isNull = (value == (id)kCFNull);
        switch (meta->_type & YYEncodingTypeMask) {
            // id
            case YYEncodingTypeObject: {
                if (isNull) { // 空,賦值 nil
                    ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
                } else if ([value isKindOfClass:meta->_cls] || !meta->_cls) {
                    // 屬性元與 value 從屬於同一個類,則直接賦值
                    ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value);
                } else if ([value isKindOfClass:[NSDictionary class]]) {
                    // 嘛~ value 從屬於 
                    NSObject *one = nil;
                    // 如果屬性元有 getter 方法,則通過 getter 獲取到例項
                    if (meta->_getter) {
                        one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);
                    }
                    if (one) {
                        // 用 yy_modelSetWithDictionary 輸出化屬性例項物件
                        [one yy_modelSetWithDictionary:value];
                    } else {
                        Class cls = meta->_cls;
                        // 略過
                        if (meta->_hasCustomClassFromDictionary) {
                            cls = [cls modelCustomClassForDictionary:value];
                            if (!cls) cls = meta->_genericCls; // for xcode code coverage
                        }
                        // 用 yy_modelSetWithDictionary 輸出化屬性例項物件,賦值
                        one = [cls new];
                        [one yy_modelSetWithDictionary:value];
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one);
                    }
                }
            } break;
            
            // Class
            case YYEncodingTypeClass: {
                if (isNull) { // 空,賦值(Class)NULL,由於 Class 其實是 C 語言定義的結構體,所以使用 NULL
                    // 關於 nil,Nil,NULL,NSNull,kCFNull 的橫向比較,我會單獨拎出來在下面介紹
                    ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)NULL);
                } else {
                    // 判斷 value 可能的型別 NSString 或判斷 class_isMetaClass(object_getClass(value))
                    // 如果滿足條件則賦值
                    ...
                }
            } break;
            
            // SEL
            case  YYEncodingTypeSEL: {
                // 判空,賦值(SEL)NULL
                // 否則轉換型別 SEL sel = NSSelectorFromString(value); 然後賦值
                ...
            } break;
                
            // block
            case YYEncodingTypeBlock: {
                // 判空,賦值(void (^)())NULL
                // 否則判斷型別 [value isKindOfClass:YYNSBlockClass()] 之後賦值
                ...
            } break;
            
            // struct、union、char[n],關於 union 共同體感興趣的同學可以自己 google,這裡簡單介紹一下
            // union 共同體,類似 struct 的存在,但是 union 每個成員會用同一個儲存空間,只能儲存最後一個成員的資訊
            case YYEncodingTypeStruct:
            case YYEncodingTypeUnion:
            case YYEncodingTypeCArray: {
                if ([value isKindOfClass:[NSValue class]]) { 
                    // 涉及 Type Encodings
                    const char *valueType = ((NSValue *)value).objCType;
                    const char *metaType = meta->_info.typeEncoding.UTF8String;
                    // 比較 valueType 與 metaType 是否相同,相同(strcmp(a, b) 返回 0)則賦值
                    if (valueType && metaType && strcmp(valueType, metaType) == 0) {
                        [model setValue:value forKey:meta->_name];
                    }
                }
            } break;
            
            // void* 或 char*
            case YYEncodingTypePointer:
            case YYEncodingTypeCString: {
                if (isNull) { // 判空,賦值(void *)NULL
                    ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, (void *)NULL);
                } else if ([value isKindOfClass:[NSValue class]]) {
                    // 涉及 Type Encodings
                    NSValue *nsValue = value;
                    if (nsValue.objCType && strcmp(nsValue.objCType, "^v") == 0) {
                        ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, nsValue.pointerValue);
                    }
                }
            }
                
            default: break;
        }
    }
}
複製程式碼

額 ? 我是真的已經忽略掉很多程式碼了,沒辦法還是有點長。其實程式碼邏輯還是很簡單的,只是模型賦值涉及的編碼型別等瑣碎邏輯比較多導致程式碼量比較大,我們一起來總結一下核心程式碼的實現邏輯。

  • 根據屬性元型別劃分程式碼邏輯
  • 如果屬性元是 CNumber 型別,即 int、uint 之類,則使用 ModelSetNumberToProperty 賦值
  • 如果屬性元屬於 NSType 型別,即 NSString、NSNumber 之類,則根據型別轉換中可能涉及到的對應型別做邏輯判斷並賦值(可以去上面程式碼中檢視具體實現邏輯)
  • 如果屬性元不屬於 CNumber 和 NSType,則猜測為 id,Class,SEL,Block,struct、union、char[n],void* 或 char* 型別並且做出相應的轉換和賦值

嘛~ 其實上面的程式碼除了長以外邏輯還是很簡單的,總結起來就是根據可能出現的型別去做出對應的邏輯操作,建議各位有時間還是去讀下原始碼,尤其是自己專案中用到 YYModel 的同學。相信看完之後會對 YYModel 屬性賦值一清二楚,這樣在使用 YYModel 的日常中出現任何問題都可以心中有數,改起程式碼自然如有神助哈。

額…考慮到 NSDictionary to Model 的整個過程程式碼量不小,我花了一些時間將其邏輯總結歸納為一張圖:

揭祕 YYModel 的魔法(下)

希望可以儘自己的努力讓文章的表述變得更直白。

Model to JSON

揭祕 YYModel 的魔法(下)

相比於 JSON to Model 來說,Model to JSON 更簡單一些。其中因為 NSJSONSerialization 在對 JSON 的轉換時做了一些規定:

  • 頂級物件是 NSArray 或者 NSDictionary 型別
  • 所有的物件都是 NSString, NSNumber, NSArray, NSDictionary, 或 NSNull 的例項
  • 所有字典中的 key 都是一個 NSString 例項
  • Numbers 是除去無窮大和 NaN 的其他表示

Note: 上文出自 NSJSONSerialization 官方文件

知道了這一點後,我們就可以從 YYModel 的 Model to JSON 介面 yy_modelToJSONObject 處開始解讀原始碼了。

- (id)yy_modelToJSONObject {
    // 遞迴轉換模型到 JSON
    id jsonObject = ModelToJSONObjectRecursive(self);
    if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
    if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;
    
    return nil;
}
複製程式碼

嘛~ 一共 4 行程式碼,只需要關注一下第一行程式碼中的 ModelToJSONObjectRecursive 方法,Objective-C 的語言特性決定了從函式名稱即可無需註釋看懂程式碼,這個方法從名字上就可以 get 到它是通過遞迴方法使 Model 轉換為 JSON 的。

// 遞迴轉換模型到 JSON,如果轉換異常則返回 nil
static id ModelToJSONObjectRecursive(NSObject *model) {
    // 判空或者可以直接返回的物件,則直接返回
    if (!model || model == (id)kCFNull) return model;
    if ([model isKindOfClass:[NSString class]]) return model;
    if ([model isKindOfClass:[NSNumber class]]) return model;
    // 如果 model 從屬於 NSDictionary
    if ([model isKindOfClass:[NSDictionary class]]) {
        // 如果可以直接轉換為 JSON 資料,則返回
        if ([NSJSONSerialization isValidJSONObject:model]) return model;
        NSMutableDictionary *newDic = [NSMutableDictionary new];
        // 遍歷 model 的 key 和 value
        [((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
            NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
            if (!stringKey) return;
            // 遞迴解析 value 
            id jsonObj = ModelToJSONObjectRecursive(obj);
            if (!jsonObj) jsonObj = (id)kCFNull;
            newDic[stringKey] = jsonObj;
        }];
        return newDic;
    }
    // 如果 model 從屬於 NSSet
    if ([model isKindOfClass:[NSSet class]]) {
        // 如果能夠直接轉換 JSON 物件,則直接返回
        // 否則遍歷,按需要遞迴解析
        ...
    }
    if ([model isKindOfClass:[NSArray class]]) {
        // 如果能夠直接轉換 JSON 物件,則直接返回
        // 否則遍歷,按需要遞迴解析
        ...
    }
    // 對 NSURL, NSAttributedString, NSDate, NSData 做相應處理
    if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
    if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
    if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model];
    if ([model isKindOfClass:[NSData class]]) return nil;
    
    // 用 [model class] 初始化一個模型元
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]];
    // 如果對映表為空,則不做解析直接返回 nil
    if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil;
    // 效能優化細節,使用 __unsafe_unretained 來避免在下面遍歷 block 中直接使用 result 指標造成的不必要 retain 與 release 開銷
    NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64];
    __unsafe_unretained NSMutableDictionary *dic = result;
    // 遍歷模型元屬性對映字典
    [modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
        // 如果遍歷當前屬性元沒有 getter 方法,跳過
        if (!propertyMeta->_getter) return;
        
        id value = nil;
        // 如果屬性元屬於 CNumber,即其 type 是 int、float、double 之類的
        if (propertyMeta->_isCNumber) {
            // 從屬性中利用 getter 方法得到對應的值
            value = ModelCreateNumberFromProperty(model, propertyMeta);
        } else if (propertyMeta->_nsType) { // 屬性元屬於 nsType,即 NSString 之類
            // 利用 getter 方法拿到 value
            id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
            // 對拿到的 value 遞迴解析
            value = ModelToJSONObjectRecursive(v);
        } else {
            // 根據屬性元的 type 做相應處理
            switch (propertyMeta->_type & YYEncodingTypeMask) {
                // id,需要遞迴解析,如果解析失敗則返回 nil
                case YYEncodingTypeObject: {
                    id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                    value = ModelToJSONObjectRecursive(v);
                    if (value == (id)kCFNull) value = nil;
                } break;
                // Class,轉 NSString,返回 Class 名稱
                case YYEncodingTypeClass: {
                    Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                    value = v ? NSStringFromClass(v) : nil;
                } break;
                // SEL,轉 NSString,返回給定 SEL 的字串表現形式
                case YYEncodingTypeSEL: {
                    SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                    value = v ? NSStringFromSelector(v) : nil;
                } break;
                default: break;
            }
        }
        // 如果 value 還是沒能解析,則跳過
        if (!value) return;
        
        // 當前屬性元是 KeyPath 對映,即 a.b.c 之類
        if (propertyMeta->_mappedToKeyPath) {
            NSMutableDictionary *superDic = dic;
            NSMutableDictionary *subDic = nil;
            // _mappedToKeyPath 是 a.b.c 根據 `.` 拆分成的字串陣列,遍歷 _mappedToKeyPath
            for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
                NSString *key = propertyMeta->_mappedToKeyPath[i];
                // 遍歷到結尾
                if (i + 1 == max) {
                    // 如果結尾的 key 為 nil,則使用 value 賦值
                    if (!superDic[key]) superDic[key] = value;
                    break;
                }
                
                // 用 subDic 拿到當前 key 對應的值
                subDic = superDic[key];
                // 如果 subDic 存在
                if (subDic) {
                    // 如果 subDic 從屬於 NSDictionary
                    if ([subDic isKindOfClass:[NSDictionary class]]) {
                        // 將 subDic 的 mutable 版本賦值給 superDic[key]
                        subDic = subDic.mutableCopy;
                        superDic[key] = subDic;
                    } else {
                        break;
                    }
                } else {
                    // 將 NSMutableDictionary 賦值給 superDic[key]
                    // 注意這裡使用 subDic 間接賦值是有原因的,原因就在下面
                    subDic = [NSMutableDictionary new];
                    superDic[key] = subDic;
                }
                // superDic 指向 subDic,這樣在遍歷 _mappedToKeyPath 時即可逐層解析
                // 這就是上面先把 subDic 轉為 NSMutableDictionary 的原因
                superDic = subDic;
                subDic = nil;
            }
        } else {
            // 如果不是 KeyPath 則檢測 dic[propertyMeta->_mappedToKey],如果為 nil 則賦值 value
            if (!dic[propertyMeta->_mappedToKey]) {
                dic[propertyMeta->_mappedToKey] = value;
            }
        }
    }];
    
    // 忽略,對應 modelCustomTransformToDictionary 介面
    if (modelMeta->_hasCustomTransformToDictionary) {
        // 用於在預設的 Model 轉 JSON 過程不適合當前 Model 型別時提供自定義額外過程
        // 也可以用這個方法來驗證轉換結果
        BOOL suc = [((id<YYModel>)model) modelCustomTransformToDictionary:dic];
        if (!suc) return nil;
    }
    
    return result;
}
複製程式碼

額…程式碼還是有些長,不過相比於之前 JSON to Model 方向上由 yy_modelSetWithDictionaryModelSetWithDictionaryFunctionModelSetValueForProperty 三個方法構成的間接遞迴來說算是非常簡單了,那麼總結一下上面的程式碼邏輯。

  • 判斷入參,如果滿足條件可以直接返回
  • 如果 Model 從屬於 NSType,則根據不同的型別做邏輯處理
  • 如果上面條件不被滿足,則用 Model 的 Class 初始化一個模型元 _YYModelMeta
  • 判斷模型元的對映關係,遍歷對映表拿到對應鍵值對並存入字典中並返回

Note: 這裡有一個效能優化的細節,用 __unsafe_unretained 修飾的 dic 指向我們最後要 return 的 NSMutableDictionary *result,看作者的註釋:// avoid retain and release in block 是為了避免直接使用 result 在後面遍歷對映表的程式碼塊中不必要的 retain 和 release 操作以節省開銷。

總結

  • 文章緊接上文《揭祕 YYModel 的魔法(上)》中對 YYModel 程式碼結構的講解後將重點放到了對 JSON 模型相互轉換的實現邏輯上。
  • 從 JSON 模型的轉換方向上劃分,將 YYModel 的 JSON 模型轉換過程正反方向剖析揭祕,希望可以解開大家對 JSON 模型自動轉換的疑惑。

文章寫的比較用心(是我個人的原創文章,轉載請註明 lision.me/),如果發現錯誤會優先在我的 個人部落格 中更新。

如果對文章有哪些意見可以直接在我的微博 @Lision 聯絡我(因為社群發文之後的通知太多了,所以我把這些 push 給關了只留了微博,微博冷清~~~~(>_<)~~~~)。


補充~ 我建了一個技術交流微信群,想在裡面認識更多的朋友!如果各位同學對文章有什麼疑問或者工作之中遇到一些小問題都可以在群裡找到我或者其他群友交流討論,期待你的加入喲~

揭祕 YYModel 的魔法(下)

Emmmmm..由於微信群人數過百導致不可以掃碼入群,所以請掃描上面的二維碼關注公眾號進群。

相關文章