《YYModel源碼分析(二)NSObject+YYModel》

承接上文《YYModel源碼分析(一)YYClassInfo》 以前文章講述了YYClassInfo如何將runtime類結構封裝到OC層。這篇文章主要講述YYModel是如何用NSObject分類,實現非侵入式json-model的(類型轉換,容錯,model轉json會在其餘文章中討論)。json

寫在開頭

NSObject+ YYModel中並不僅有NSObject分類,還包含了_YYModelPropertyMeta_YYModelMeta以及協議<YYModel>,固然又聲明瞭不少靜態(內聯)函數,至於爲何用內聯函數而不用類方法或者宏定義,是由於內聯函數在編譯中會將代碼插入到調用的位置,這樣會提升調用效率,相對於宏又有函數的特色。具體能夠看這裏《IOS 內聯函數Q&A》數組

協議

首先字典轉模型,就是字典中key對應的value賦值給model對應的屬性的過程,默認狀況下咱們都會將屬性名對應成字典的key,那麼若是咱們不想這麼起名字。或者咱們有這樣一個json:bash

{
         "n":"Harry Pottery",
         "p": 256,
         "ext" : {
             "desc" : "A book written by J.K.Rowling."
         },
         "ID" : 100010
 }
複製代碼

咱們想賦值給這個modelapp

@interface YYBook : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
複製代碼

要實現以上的需求就必須告訴YYModel屬性應該如何取值,<YYModel>提供了這樣一套規範協議。接下來咱們依次看一下函數

/**
 返回一個map,key是屬性名,value是json中對應的key,能夠有三種形式。
 
 @{@"name"  : @"n",                         //對應一個json中的key
   @"desc"  : @"ext.desc",                  //對應一個json地址。
   @"bookID": @[@"id", @"ID", @"book_id"]}; //對應多個json中的key。
 */
+ (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;
/**
 告訴YYModel容器類型中元素的類型。以下:
 @{@"shadows" : [YYShadow class],
   @"borders" : YYBorder.class,
   @"attachments" : @"YYAttachment" }
 value能夠穿Class也能夠穿字符串,能夠自動解析
 */
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
/**
想根據dictionary提供的數據建立不一樣的類,實現這個方法,會根據返回的類型建立對象
注意這個協議對`+modelWithJSON:`, `+modelWithDictionary:`,這兩個方法有效
 */
+ (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;
/**
 在json轉model的時候,黑名單上的屬性都會被忽略
 */
+ (nullable NSArray<NSString *> *)modelPropertyBlacklist;
/**
 在json轉model的時候,若是屬性沒有在白名單上,將會被忽略。
 */
+ (nullable NSArray<NSString *> *)modelPropertyWhitelist;
/**
 這個方法能夠在json轉model以前對dic進行更改,json轉model將按照返回的dic爲準。
 */
- (NSDictionary *)modelCustomWillTransformDictionary:(NSDictionary *)dic;
/**
 該接口會在json轉model以後調用,用於不適合模型對象時作額外的邏輯處理。咱們也能夠用這個接口來驗證模型轉換的結果
 */
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;
複製代碼

靜態函數

在NSObject+YYModel.m文件中一看,差很少一半都是靜態(內聯)函數,內聯函數咱們前面已經說過了,static修飾函數跟普通函數有如下區別:源碼分析

  • 語法與C++保持一致,只在模塊內部可見
  • 跟類無關,因此也沒法調用self,只能根據參數實現相關功能
  • 靜態參數不參與動態派發,沒有再函數列表裏,靜態綁定 因此由於要頻繁調用,因此尋求更高效的static函數。我把靜態函數和其功能都列在下面了,供參考。
//將類解析成Foundation類型,傳入Class返回枚舉YYEncodingNSType
static force_inline YYEncodingNSType YYClassGetNSType(Class cls) 
//經過YYEncodingType判斷是不是c數字類型
static force_inline BOOL YYEncodingTypeIsCNumber(YYEncodingType type)
//將一個ID類型的數據解析成NSNumber,這裏主要處理了字符串轉數字的狀況
static force_inline NSNumber *YYNSNumberCreateFromID(__unsafe_unretained id value)
//NSString類型數據轉NSDate,這裏幾乎兼容了全部時間格式,而且作了容錯
static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string)
//獲取NSBlock這個類,加入了打印咱們能夠看出 block 的父類的關係是block -------> NSGlobalBlock ---------> NSBlock
static force_inline Class YYNSBlockClass() 
//獲取ISO時間格式
static force_inline NSDateFormatter *YYISODateFormatter()
//根據KeyPath獲取一個字典中的數據
static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) 
//一句多個Key從字典中獲取數據,這裏若是有一個Key有值就取值返回。
static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) 
//
static force_inline NSNumber *ModelCreateNumberFromProperty(__unsafe_unretained id model,
                                                            __unsafe_unretained _YYModelPropertyMeta *meta)
//爲一個對象設置數值屬性
static force_inline void ModelSetNumberToProperty(__unsafe_unretained id model,
                                                  __unsafe_unretained NSNumber *num,
                                                  __unsafe_unretained _YYModelPropertyMeta *meta)
//爲對象的屬性賦值
static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta)
//經過鍵值爲_context設置屬性,_context是一個結構體,後面咱們會講到,包含了數據源dic、model和_YYModelMeta。
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context)
//爲對象的_propertyMeta屬性賦值。
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) 
//由model返回一個有效的json。
static id ModelToJSONObjectRecursive(NSObject *model) 
複製代碼

關於這些方法的實現,後面用到會細說。測試

_YYModelPropertyMeta

其實_YYModelPropertyMeta類型是在YYClassPropertyInfo的基礎上的進一步解析而且關聯了從<YYModel>協議中的取值信息。ui

/// A property info in object model.
@interface _YYModelPropertyMeta : NSObject {
    @package
    NSString *_name;             ///< 屬性名
    YYEncodingType _type;        ///< 屬性類型,OC類型統一爲YYEncodingTypeObject
    YYEncodingNSType _nsType;    ///< 屬性的Foundation類型,NSString等等。
    BOOL _isCNumber;             ///< 是不是c數字類型
    Class _cls;                  ///< 屬性類型,
    Class _genericCls;           ///< 若是是容器類型,是容器類型內元素的類型,若是不是容器類型爲nil。
    SEL _getter;                 ///< getter方法
    SEL _setter;                 ///< setter方法
    BOOL _isKVCCompatible;       ///< 是否可使用KVC
    BOOL _isStructAvailableForKeyedArchiver; ///< 結構體是否支持歸檔解擋
    BOOL _hasCustomClassFromDictionary; ///< 是否實現了 +modelCustomClassForDictionary:協議
    
    NSString *_mappedToKey;      ///< 代表該屬性取數據源中_mappedToKey對應的value的值。
    NSArray *_mappedToKeyPath;   ///< 代表該屬性取數據源中_mappedToKeyPath對應路徑的value值,若是爲nil說明沒有關鍵路徑
    NSArray *_mappedToKeyArray;  ///< key或者keyPath的數組,代表可從多個key中取值。
    YYClassPropertyInfo *_info;  ///< 屬性信息
    _YYModelPropertyMeta *_next; ///< 下一個元數據,若是有多個屬性映射到同一個鍵。
}
@end
複製代碼

_YYModelPropertyMeta屬性咱們能夠看出,若是屬性是Foundation類型,會被解析成具體的OC類型,用枚舉的形式存儲在_nstype中,同時由Model實現的<YYModel>協議能夠獲取到取值信息_mappedToKey_mappedToKeyPath _mappedToKeyArray信息,這個在以後的賦值操做中起着相當重要的做用。編碼

@implementation _YYModelPropertyMeta

+ (instancetype)metaWithClassInfo:(YYClassInfo *)classInfo propertyInfo:(YYClassPropertyInfo *)propertyInfo generic:(Class)generic {
    // 這裏有些許疑惑,generic是當屬性是容器類時,容器類中包含的元素,代碼邏輯是若是generic爲空,且propertyInfo.protocols不爲空,若是propertyInfo.protocols中的元素是Class的時候將此class賦值給generic,可是propertyInfo.protocols確實存儲的是協議,propertyInfo.protocols的解析過程是取objc_property_attribute_t中<>中的字符,可是經測試只有一個屬性遵循了某種協議纔會出現<>字符,NSSArray<NSString*> *這樣的屬性編碼字符串也是@"NSSArray",因此這塊貌似沒什麼用。
    if (!generic && propertyInfo.protocols) {
        //
        for (NSString *protocol in propertyInfo.protocols) {
            Class cls = objc_getClass(protocol.UTF8String);
            if (cls) {
                generic = cls;
                break;
            }
        }
    }
    
    _YYModelPropertyMeta *meta = [self new];
    //給meta的成員變量賦值
    meta->_name = propertyInfo.name;
    //類型枚舉
    meta->_type = propertyInfo.type;
    //存儲屬性元數據
    meta->_info = propertyInfo;
    //容器類包含的通用類型
    meta->_genericCls = generic;
    //若是屬性是OC類型的
    if ((meta->_type & YYEncodingTypeMask) == YYEncodingTypeObject) {
        //解析成枚舉
        meta->_nsType = YYClassGetNSType(propertyInfo.cls);
    } else {
        //判斷是不是number類
        meta->_isCNumber = YYEncodingTypeIsCNumber(meta->_type);
    }
    //若是是結構圖
    if ((meta->_type & YYEncodingTypeMask) == YYEncodingTypeStruct) {
        static NSSet *types = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSMutableSet *set = [NSMutableSet new];
            // 32 bit
            [set addObject:@"{CGSize=ff}"];
            [set addObject:@"{CGPoint=ff}"];
            [set addObject:@"{CGRect={CGPoint=ff}{CGSize=ff}}"];
            [set addObject:@"{CGAffineTransform=ffffff}"];
            [set addObject:@"{UIEdgeInsets=ffff}"];
            [set addObject:@"{UIOffset=ff}"];
            // 64 bit
            [set addObject:@"{CGSize=dd}"];
            [set addObject:@"{CGPoint=dd}"];
            [set addObject:@"{CGRect={CGPoint=dd}{CGSize=dd}}"];
            [set addObject:@"{CGAffineTransform=dddddd}"];
            [set addObject:@"{UIEdgeInsets=dddd}"];
            [set addObject:@"{UIOffset=dd}"];
            types = set;
        });
        //若是是以上結構體則支持歸解檔
        if ([types containsObject:propertyInfo.typeEncoding]) {
            meta->_isStructAvailableForKeyedArchiver = YES;
        }
    }
    meta->_cls = propertyInfo.cls;
    
    if (generic) {
        //容器類元素是否實現了 modelCustomClassForDictionary協議
        meta->_hasCustomClassFromDictionary = [generic respondsToSelector:@selector(modelCustomClassForDictionary:)];
    } else if (meta->_cls && meta->_nsType == YYEncodingTypeNSUnknown) {
        meta->_hasCustomClassFromDictionary = [meta->_cls respondsToSelector:@selector(modelCustomClassForDictionary:)];
    }
    
    //設置getter方法
    if (propertyInfo.getter) {
        if ([classInfo.cls instancesRespondToSelector:propertyInfo.getter]) {
            meta->_getter = propertyInfo.getter;
        }
    }
    //設置setter方法
    if (propertyInfo.setter) {
        if ([classInfo.cls instancesRespondToSelector:propertyInfo.setter]) {
            meta->_setter = propertyInfo.setter;
        }
    }
    
    if (meta->_getter && meta->_setter) {
        /*
         如下類型都不支持KVC
         */
        switch (meta->_type & YYEncodingTypeMask) {
            case YYEncodingTypeBool:
            case YYEncodingTypeInt8:
            case YYEncodingTypeUInt8:
            case YYEncodingTypeInt16:
            case YYEncodingTypeUInt16:
            case YYEncodingTypeInt32:
            case YYEncodingTypeUInt32:
            case YYEncodingTypeInt64:
            case YYEncodingTypeUInt64:
            case YYEncodingTypeFloat:
            case YYEncodingTypeDouble:
            case YYEncodingTypeObject:
            case YYEncodingTypeClass:
            case YYEncodingTypeBlock:
            case YYEncodingTypeStruct:
            case YYEncodingTypeUnion: {
                meta->_isKVCCompatible = YES;
            } break;
            default: break;
        }
    }
    
    return meta;
}
@end
複製代碼

_YYModelMeta

_YYModelMeta經過Model遵循的<YYModel>協議,收集取值信息,並映射到_YYModelPropertyMeta當中,將其中有效的信息封裝到該類中。spa

@interface _YYModelMeta : NSObject {
    //@package當前framework可使用,外部不能夠
    @package
    
    YYClassInfo *_classInfo;
    /// [key:_YYModelPropertyMeta]
    NSDictionary *_mapper;
    /// 全部的屬性_YYModelPropertyMeta數據,這裏包含當前類到跟類NSObject中的全部屬性
    NSArray *_allPropertyMetas;
    /// 映射到KeyPath的屬性_keyPathPropertyMetas集合
    NSArray *_keyPathPropertyMetas;
    /// 映射到多個鍵值的屬性_keyPathPropertyMetas集合
    NSArray *_multiKeysPropertyMetas;
    /// 屬性映射的數量。
    NSUInteger _keyMappedCount;
    /// Foundation類型
    YYEncodingNSType _nsType;
    
    BOOL _hasCustomWillTransformFromDictionary;
    BOOL _hasCustomTransformFromDictionary;
    BOOL _hasCustomTransformToDictionary;
    BOOL _hasCustomClassFromDictionary;
}
@end
複製代碼

接下來討論一下_YYModelMet是如何初始化的。過程以下

  • 1.從實現的modelPropertyBlacklist、modelPropertyWhitelist協議中獲取取值黑名單、白名單。
  • 2.從實現的modelContainerPropertyGenericClass協議中獲取容器類屬性中的元素類型
  • 3.獲取當前類及繼承鏈直至NSObject中全部的屬性生成_YYModelPropertyMeta對象,存儲到allPropertyMetas
  • 4.從實現的modelCustomPropertyMapper協議中獲取自定義map,這裏map的key是屬性名,value有三種狀況,第一是對應一個取值key,第二是一個keypath用'.'隔開,第三是一個字符數組對應多個取值key
  • 5.遍歷map,由mapkey取出對應的propertyMeta而後根據步驟4中value的三種狀況給propertyMeta_mappedToKey、_mappedToKeyPath、_mappedToKeyArray賦值,這樣就把屬性和取值邏輯綁定在了一塊兒
  • 6.給_keyMappedCount賦值,查看modelCustomWillTransformFromDictionary、modelCustomTransformFromDictionary、modelCustomTransformToDictionary 、modelCustomClassForDictionary這四個協議是否實現。

這個過程代碼比較多,就不列出來了。感興趣的能夠本身看下哈。

NSObject (YYModel)

NSObject (YYModel)是YYModel非侵入式的關鍵,模型對象經過調用擴展方法實現json轉model。接下來咱們用json-model的核心方法yy_modelWithDictionary舉例。

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    //容錯處理
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    //獲取當前類的類型
    Class cls = [self class];
    //建立_YYModelMeta
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    //這裏建立_YYModelMeta的目的就是查看是否實現了modelCustomClassForDictionary協議,哈哈,這裏回溯一下modelCustomClassForDictionary的功能,這個協議你能夠根據dictionary數據建立一個不一樣於當前類的對象來完成json轉model。
    if (modelMeta->_hasCustomClassFromDictionary) {
        //若是實現了這個協議則替換當前類型。
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    //由獲取到的類型建立對象
    NSObject *one = [cls new];
    //調用yy_modelSetWithDictionary方法。
    if ([one yy_modelSetWithDictionary:dictionary]) return one;
    return nil;
}
複製代碼

再看一下屬性賦值的方法yy_modelSetWithDictionary

- (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)];
    if (modelMeta->_keyMappedCount == 0) return NO;
    //查看是否實現modelCustomWillTransformFromDictionary協議,若是實現調用該方法,處理dic
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }
    //建立ModelSetContext,一個結構體
    //    typedef struct {
    //        void *modelMeta;  ///< _YYModelMeta
    //        void *model;      ///< id (self)
    //        void *dictionary; ///< NSDictionary (json)
    //    } ModelSetContext;
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic);
        //若是自定義的鍵值數量大於等於數據源的鍵值數量,那麼按照自定義鍵值處理
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        //CFDictionaryApplyFunction意思是爲字典中的每一個鍵值對調用一次函數
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            //處理取值爲_keyPathPropertyMetas形式的屬性
            //CFArrayApplyFunction是爲數組中的每一個元素對調用一次函數。
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        if (modelMeta->_multiKeysPropertyMetas) {
            //處理取值爲_multiKeysPropertyMetas形式的屬性
            CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
    } else {
        //若是自定義鍵值數量小於數據源的鍵值數量,那麼直接按照dic key值給屬性賦值,自定義的無效
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    if (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}
複製代碼

經過以上代碼邏輯咱們知道,若是沒有設置全量鍵值映射,也就是說實際數據源的鍵值數量大於自定義鍵值數量,那麼自定義鍵值無效,會直接按照實際數據源的key對應屬性名進行賦值。

咱們能夠看到賦值操做中有兩個比較重要的方法ModelSetWithDictionaryFunction,ModelSetWithPropertyMetaArrayFunction

/**
 經過鍵值給模型賦值
 
 @param _key     鍵
 @param _value   值
 @param _context 賦值必要的數據,model,modelMeta,dictionary
 */
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);
    while (propertyMeta) {
        if (propertyMeta->_setter) {
            ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
        }
        propertyMeta = propertyMeta->_next;
    };
}
/**
 爲模型的某一個屬性賦值
 
 @param _propertyMeta 屬性
 @param _context   賦值必要的數據,model,modelMeta,dictionary
 */
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary);
    __unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta);
    if (!propertyMeta->_setter) return;
    id value = nil;
    
    if (propertyMeta->_mappedToKeyArray) {
        value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
    } else if (propertyMeta->_mappedToKeyPath) {
        value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
    } else {
        value = [dictionary objectForKey:propertyMeta->_mappedToKey];
    }
    
    if (value) {
        __unsafe_unretained id model = (__bridge id)(context->model);
        ModelSetValueForProperty(model, value, propertyMeta);
    }
}
複製代碼

能夠看到這兩個方法同歸,在取到值以後都調用了ModelSetValueForProperty的方法,這個纔是真正屬性賦值的方法。這個函數作的就是經過runtime函數objc_msgSend調用對象的setter方法賦值,之因此代碼量巨大是由於對全部的數據類型(c數字,foundation類型)作了判斷並添加了大量的容錯。關於類型轉換和容錯以後會單獨出一篇文章談論。

總結

  • YYModel經過擴展實現了無侵入式操做
  • 協議使Model與YYModel進行數據交互
  • YYClassInfo封裝Model類型的runtime數據
  • _YYModelPropertyMeta將屬性與取值信息綁定
  • _YYModelMeta封裝全部的_YYModelPropertyMeta屬性
  • 最後經過runtime接口調用屬性對應的setter方法賦值
相關文章
相關標籤/搜索