閱讀YYModel

aron1992發表於2019-04-04

YYModel庫中涉及到Runtime、CF API、訊號和鎖、位的操作。學習該庫可以學習到使用Runtime獲取類的資訊,包括:類屬性資訊、類ivar資訊、類方法、型別編碼;使用runtime底層技術進行方法呼叫,也就是objc_msgSend方法的使用;dispatch_semaphore_t訊號鎖的使用;CF框架中CFMutableDictionaryRef/CFMutableDictionaryRef物件的操作;位的操作。

  • 簡單使用介紹
    • 簡單的轉化
    • 自定義屬性和json欄位的對映配置
    • 黑名單和白名單配置
    • 型別對映配置
  • 預備知識
    • Type Encodings
    • Property Type String
  • 程式碼解析
    • 類資訊的獲取
    • 配置資訊的獲取
    • 模型物件的轉換
  • 其它重要知識點
    • 位操作
    • 訊號和鎖
    • CF框架API

YYModel使用介紹

首先會介紹下YYModel的使用,作為下面程式碼解析章節的鋪墊,程式碼解析章節以使用方式作為入口點切入,研究框架整體的實現思路、步驟以及每個步驟使用的詳細技術。

簡單的轉化

類屬性的定義和json資料中的key是一致的,這種情況最為簡單,不用配置對映關係,使用NSObject+YYModel的方法yy_modelWithDictionary獲取yy_modelWithJSON即可以把資料反序列化為物件。

比如元素的JSON資料如下所示:

    {
      "id": 7975492,
      "title": "同學們,開學了,準備好早起了嗎?\n",
      "source_text": "來自新浪微博",
      "share_count": 1,
      "praise_num": 999,
      "comment_num": 0,
      "is_praise": 0
    }
複製程式碼

定義的資料模型類如下:

@interface IMYOriginalRecommendWeiboModel : IMYRecommendBaseModel <YYModel>

@property (nonatomic, copy) NSString *source_text;
@property (nonatomic, assign) NSInteger share_count;
@property (nonatomic, assign) NSInteger praise_num; ///< 點贊數
@property (nonatomic, assign) BOOL is_praise; ///< 是否已點贊
@property (nonatomic, assign) NSInteger comment_num; ///< 評論數量

@end
複製程式碼

轉換後的結果如下:

簡單的轉化-轉換後的結果1
簡單的轉化-轉換後的結果1

自定義屬性和json欄位的對映配置

多數情況下,服務端的介面是為了多平臺開發的,不同平臺的屬性定義標準、格式不一樣,多數情況下我們需要把服務端的資料個數對映為平臺適應的格式,這種情況需要用到配置自定義屬性和json欄位的對映,可以重寫YYModel協議的方法modelCustomPropertyMapper返回一個配置。

定義的資料模型類如下:

@interface IMYOriginalRecommendWeiboModel : IMYRecommendBaseModel <YYModel>

@property (nonatomic, copy) NSString *sourceText;
@property (nonatomic, assign) NSInteger shareCount;
@property (nonatomic, assign) NSInteger praiseCount; ///< 點贊數
@property (nonatomic, assign) BOOL isPraise; ///< 是否已點贊
@property (nonatomic, assign) NSInteger commentCount; ///< 評論數量

@end
複製程式碼

對映關係配置如下:

@implementation IMYRecommendWeiboModel

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"sourceText"  : @"source_text",
             @"shareCount"  : @"share_count",
             @"praiseCount"  : @"praise_num",
             @"isPraise"  : @"is_praise",
             @"commentCount"  : @"comment_num",
             };
}

@end
複製程式碼

轉換後的結果如下:

自定義屬性和json欄位的對映配置-轉換後的結果2
自定義屬性和json欄位的對映配置-轉換後的結果2

黑名單和白名單配置

黑名單配置如下:

@implementation IMYRecommendBlackListWeiboModel

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"sourceText"  : @"source_text",
             @"shareCount"  : @"share_count",
             @"praiseCount"  : @"praise_num",
             @"isPraise"  : @"is_praise",
             @"commentCount"  : @"comment_num",
             };
}

+ (NSArray<NSString *> *)modelPropertyBlacklist {
    return @[@"sourceText", @"shareCount"];
}

@end
複製程式碼

反序列化的結果,配置在黑名單中的屬性(sourceTextshareCount)不會被賦值

黑名單配置-轉換後的結果3
黑名單配置-轉換後的結果3

白名單配置如下:

@implementation IMYRecommendWhiteListWeiboModel

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"sourceText"  : @"source_text",
             @"shareCount"  : @"share_count",
             @"praiseCount"  : @"praise_num",
             @"isPraise"  : @"is_praise",
             @"commentCount"  : @"comment_num",
             };
}

+ (NSArray<NSString *> *)modelPropertyWhitelist {
    return @[@"sourceText", @"shareCount"];
}
@end
複製程式碼

反序列化的結果,只有配置在白名單中的屬性(sourceTextshareCount)才會被賦值

白名單配置-轉換後的結果4
白名單配置-轉換後的結果4

型別對映配置

型別對映配置用於屬性是陣列型別,需要配置該屬性中元素的類型別,比如我們定義的資料模型如下,有個users指定是陣列型別

@interface IMYRecommendExtendWeiboModel : IMYRecommendBaseModel <YYModel>

@property (nonatomic, copy) NSString *sourceText;
@property (nonatomic, assign) NSInteger shareCount;
@property (nonatomic, assign) NSInteger praiseCount; ///< 點贊數
@property (nonatomic, assign) BOOL isPraise; ///< 是否已點贊
@property (nonatomic, assign) NSInteger commentCount; ///< 評論數量
@property (nonatomic, strong) NSArray<IMYUser *> *users;

@end
複製程式碼

型別對映配置如下:

@implementation IMYRecommendBlackListWeiboModel

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"sourceText" : @"source_text",
             @"shareCount" : @"share_count",
             @"praiseCount" : @"praise_num",
             @"isPraise" : @"is_praise",
             @"commentCount" : @"comment_num",
             };
}

+ (NSDictionary *)modelContainerPropertyGenericClass {
    return @{@"users" : [IMYUser class]};
}

@end
複製程式碼

反序列化的結果如下:

型別對映配置-轉換後的結果5
型別對映配置-轉換後的結果5

預備知識

首先需要了解的是蘋果對於型別說明的的一些規範,包括了Type EncodingsProperty Type String,YYModel程式碼中重要的一部分就是對這些資訊進行建模處理。

Type Encodings

Type Encodings 指的是屬性、引數、變數的型別,比如基本資料型別有char、int、short、long、等;此外還有物件型別、類型別、陣列型別、結構體型別、共用體型別、OC中的SEL型別等。其中Block 型別的比較特殊,該型別的的型別定義為:"@?",在YYModel中處理Block型別的程式碼如下:

case '@': {
    if (len == 2 && *(type + 1) == '?')
        return YYEncodingTypeBlock | qualifier;
    else
        return YYEncodingTypeObject | qualifier;
}
複製程式碼

Property Type String

Property Type String 指的是property的記憶體屬性、讀寫屬性、原子屬性、getter/getter屬性、屬性對應的ivar名稱以及屬性本身的Type Encoding,其中property的記憶體屬性、讀寫屬性、原子屬性是隻有屬性名稱,沒有屬性值,而property的getter/getter屬性、屬性對應的ivar名稱以及屬性本身的Type Encoding除了屬性名稱之外還會附加一個屬性的值。檢視property的屬性名字和值可以使用property_getAttributes方法獲取,返回的是一個const char *型別,如果需要對屬性的名稱和值進行詳細的分析可以使用property_copyAttributeList獲取到一個陣列,陣列的元素是objc_property_attribute_t這種結構體型別的,已經解析好了namevalue,方便使用。

假設定義有如下的屬性@property(getter=isIntReadOnlyGetter, readonly) int intReadonlyGetter;,使用property_getAttributes方法獲取到的屬性對應的屬性描述如下:Ti,R,GisIntReadOnlyGetterintReadonlyGetter屬性對應的objc_property_attribute_t型別為p,基本的規則解釋如下:

  • 型別 p.name == 'T',需要獲取 p.value值,然後查詢Type Encodings表格
  • 對應的ivar名稱 p.name == 'V',需要獲取 p.value值,p.value值為ivar名稱
  • 讀寫屬性 p.name == 'R' -> readonly
  • 記憶體屬性 p.name == 'C' -> copy; p.name == '&' -> retain; p.name == 'W' -> weak
  • 原子屬性 p.name == 'N' -> nonatomic
  • getter/setter屬性 p.name = 'G' -> G後面的內容為自定義的getter方法名稱,使用p.value獲取;p.name = 'S' -> S後面的內容為自定義的setter方法名稱,使用p.value獲取

以下是一段使用property_getAttributes方法獲取屬性名字和值得示例程式碼

定義一個測試的類包含有如下幾個屬性

@interface MyObj : NSObject
@property (nonatomic, copy) MyBlock block;
@property struct YorkshireTeaStruct structDefault;
@property(nonatomic, readonly, retain) id idReadonlyRetainNonatomic;
@property(getter=isIntReadOnlyGetter, readonly) int intReadonlyGetter;
@end
複製程式碼

使用如下的程式碼解析property的屬性

    char *propertyAttributes = (char *)property_getAttributes(property);
    NSString *propertyAttributesString = [NSString stringWithUTF8String:propertyAttributes];
    NSLog(@"propertyAttributesString = %@", propertyAttributesString);
複製程式碼

使用以上的程式碼對MyObj類的屬性進行分析,列印的內容如下,可以對照Property Type StringType Encodings檢視詳細的說明:

propertyAttributesString = T@?,C,N,V_block
propertyAttributesString = T{YorkshireTeaStruct=ic},V_structDefault
propertyAttributesString = T@,R,&,N,V_idReadonlyRetainNonatomic
propertyAttributesString = Ti,R,GisIntReadOnlyGetter,V_intReadonlyGetter
複製程式碼

程式碼解析

類資訊的獲取

類資訊的獲取主要是在YYClassInfo類的方法_update中處理的,包括類的類物件、父類的類物件、父類的類資訊、是否是元類、屬性資訊、方法資訊、ivar資訊。實際上方法資訊ivar資訊這兩個資訊並沒有真正的使用到,但是框架中有進行處理,為了完整性還是稍作敘述。主要使用到的還是propertyInfos屬性中的內容,後面的配置資訊獲取和模型物件轉換都需要使用到。

YYClassInfo類的定義主要如下

@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; ///< class object
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object
@property (nullable, nonatomic, assign, readonly) Class metaCls;  ///< class's meta class object
@property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class
@property (nonatomic, strong, readonly) NSString *name; ///< class name
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< properties
// ...
複製程式碼

流程時序圖如下所示:

類資訊的獲取流程時序圖
類資訊的獲取流程時序圖

獲取類資訊的操作主要都在_update方法中,主要步驟如下

  • class_copyMethodList 方法獲取類中的方法資訊,方法資訊轉換為YYClassMethodInfo物件,儲存在methodInfos屬性中
  • class_copyPropertyList 方法獲取類中的屬性資訊,屬性資訊轉換為YYClassPropertyInfo物件,儲存在propertyInfos屬性中
  • class_copyIvarList 方法獲取類中的ivar資訊,ivar資訊轉換為YYClassIvarInfo物件,儲存在ivarInfos屬性中

這裡需要注意的是class_copyMethodListclass_copyPropertyListclass_copyIvarList涉及到記憶體的拷貝問題,使用完成之後需要使用C的方法free釋放拷貝的內容,防止記憶體洩漏。上面有提到說方法資訊ivar資訊這兩個資訊並沒有真正的使用到,接下來會著重的介紹屬性資訊的獲取中的一些細節。

- (void)_update {
    _ivarInfos = nil;
    _methodInfos = nil;
    _propertyInfos = nil;
    
    Class cls = self.cls;
    // 獲取類中的方法資訊
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(cls, &methodCount);
    if (methods) {
        NSMutableDictionary *methodInfos = [NSMutableDictionary new];
        _methodInfos = methodInfos;
        for (unsigned int i = 0; i < methodCount; i++) {
            YYClassMethodInfo *info = [[YYClassMethodInfo alloc] initWithMethod:methods[i]];
            if (info.name) methodInfos[info.name] = info;
        }
        free(methods);
    }
    
    // 獲取類中的屬性資訊
    unsigned int propertyCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
    if (properties) {
        NSMutableDictionary *propertyInfos = [NSMutableDictionary new];
        _propertyInfos = propertyInfos;
        for (unsigned int i = 0; i < propertyCount; i++) {
            YYClassPropertyInfo *info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]];
            if (info.name) propertyInfos[info.name] = info;
        }
        free(properties);
    }
    
    // 獲取類中的ivar資訊
    unsigned int ivarCount = 0;
    Ivar *ivars = class_copyIvarList(cls, &ivarCount);
    if (ivars) {
        NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
        _ivarInfos = ivarInfos;
        for (unsigned int i = 0; i < ivarCount; i++) {
            YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
            if (info.name) ivarInfos[info.name] = info;
        }
        free(ivars);
    }
    
    if (!_ivarInfos) _ivarInfos = @{};
    if (!_methodInfos) _methodInfos = @{};
    if (!_propertyInfos) _propertyInfos = @{};
    
    _needUpdate = NO;
}
複製程式碼

屬性資訊獲取

屬性資訊主要包含屬性名字、屬性的Encoding 型別、屬性所屬的類、屬性的getter/setter方法的選擇子SEL。這些資訊會儲存在屬性資訊類YYClassPropertyInfo中,屬性資訊類YYClassPropertyInfo定義如下:

@interface YYClassPropertyInfo : NSObject
@property (nonatomic, assign, readonly) objc_property_t property; ///< property's opaque struct
@property (nonatomic, strong, readonly) NSString *name;           ///< property's name
@property (nonatomic, assign, readonly) YYEncodingType type;      ///< property's type
@property (nonatomic, strong, readonly) NSString *typeEncoding;   ///< property's encoding value
@property (nonatomic, strong, readonly) NSString *ivarName;       ///< property's ivar name
@property (nullable, nonatomic, assign, readonly) Class cls;      ///< may be nil
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *protocols; ///< may nil
@property (nonatomic, assign, readonly) SEL getter;               ///< getter (nonnull)
@property (nonatomic, assign, readonly) SEL setter;               ///< setter (nonnull)
// ...
複製程式碼

屬性處理主要在initWithProperty方法中進行,這部分的內容在前面的預備知識中的Type EncodingsProperty Type String有講到了大部分,主要步驟如下:

  • 獲取property名字
  • 讀取property的屬性名字和屬性值,建立屬性的Encoding模型
  • 處理getter/setter
- (instancetype)initWithProperty:(objc_property_t)property {
    if (!property) return nil;
    self = [super init];
    _property = property;
    // 獲取property名字
    const char *name = property_getName(property);
    if (name) {
        _name = [NSString stringWithUTF8String:name];
    }
    
    // 讀取property的屬性名字和屬性值,建立屬性的Encoding模型
    YYEncodingType type = 0;
    unsigned int attrCount;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    for (unsigned int i = 0; i < attrCount; i++) {
        switch (attrs[i].name[0]) {
            case 'T': { // Type encoding
                if (attrs[i].value) {
                    _typeEncoding = [NSString stringWithUTF8String:attrs[i].value];
                    type = YYEncodingGetType(attrs[i].value);
                    
                    if ((type & YYEncodingTypeMask) == YYEncodingTypeObject && _typeEncoding.length) {
                        NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
                        if (![scanner scanString:@"@\"" intoString:NULL]) continue;
                        
                        NSString *clsName = nil;
                        if ([scanner scanUpToCharactersFromSet: [NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]) {
                            if (clsName.length) _cls = objc_getClass(clsName.UTF8String);
                        }
                        
                        NSMutableArray *protocols = nil;
                        while ([scanner scanString:@"<" intoString:NULL]) {
                            NSString* protocol = nil;
                            if ([scanner scanUpToString:@">" intoString: &protocol]) {
                                if (protocol.length) {
                                    if (!protocols) protocols = [NSMutableArray new];
                                    [protocols addObject:protocol];
                                }
                            }
                            [scanner scanString:@">" intoString:NULL];
                        }
                        _protocols = protocols;
                    }
                }
            } break;
            case 'V': { // Instance variable
                if (attrs[i].value) {
                    _ivarName = [NSString stringWithUTF8String:attrs[i].value];
                }
            } break;
            case 'R': {
                type |= YYEncodingTypePropertyReadonly;
            } break;
            case 'C': {
                type |= YYEncodingTypePropertyCopy;
            } break;
            case '&': {
                type |= YYEncodingTypePropertyRetain;
            } break;
            case 'N': {
                type |= YYEncodingTypePropertyNonatomic;
            } break;
            case 'D': {
                type |= YYEncodingTypePropertyDynamic;
            } break;
            case 'W': {
                type |= YYEncodingTypePropertyWeak;
            } break;
            case 'G': {
                type |= YYEncodingTypePropertyCustomGetter;
                if (attrs[i].value) {
                    _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                }
            } break;
            case 'S': {
                type |= YYEncodingTypePropertyCustomSetter;
                if (attrs[i].value) {
                    _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                }
            } // break; commented for code coverage in next line
            default: break;
        }
    }
    if (attrs) {
        free(attrs);
        attrs = NULL;
    }
    
    _type = type;
    // 處理getter、setter方法
    if (_name.length) {
        if (!_getter) {
            _getter = NSSelectorFromString(_name);
        }
        if (!_setter) {
            _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]);
        }
    }
    return self;
}
複製程式碼

配置資訊的獲取

配置資訊的獲取主要流程時序圖如下:

配置資訊的獲取主要流程時序圖
配置資訊的獲取主要流程時序圖

主要有以下幾個步驟:

  • 黑名單配置獲取
  • 白名單配置獲取
  • 容器型別中元素型別配置獲取
  • 遞迴遍歷當前類和父類的propertyInfos屬性,獲取所有property的後設資料
  • 處理自定義的屬性對於json資料key的對映配置

對應的程式碼如下,關鍵的地方有新增了註釋:

- (instancetype)initWithClass:(Class)cls {
    YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
    if (!classInfo) return nil;
    self = [super init];
    
    // Get black list
    // 黑名單配置獲取
    NSSet *blacklist = nil;
    if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
        NSArray *properties = [(id<YYModel>)cls modelPropertyBlacklist];
        if (properties) {
            blacklist = [NSSet setWithArray:properties];
        }
    }
    
    // Get white list
    // 白名單配置獲取
    NSSet *whitelist = nil;
    if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
        NSArray *properties = [(id<YYModel>)cls modelPropertyWhitelist];
        if (properties) {
            whitelist = [NSSet setWithArray:properties];
        }
    }
    
    // Get container property's generic class
    // 容器型別中元素型別配置獲取
    NSDictionary *genericMapper = nil;
    if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
        genericMapper = [(id<YYModel>)cls modelContainerPropertyGenericClass];
        if (genericMapper) {
            NSMutableDictionary *tmp = [NSMutableDictionary new];
            [genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                if (![key isKindOfClass:[NSString class]]) return;
                Class meta = object_getClass(obj);
                if (!meta) return;
                if (class_isMetaClass(meta)) {
                    tmp[key] = obj;
                } else if ([obj isKindOfClass:[NSString class]]) {
                    Class cls = NSClassFromString(obj);
                    if (cls) {
                        tmp[key] = cls;
                    }
                }
            }];
            genericMapper = tmp;
        }
    }
    
    // 遞迴遍歷當前類和父類的propertyInfos屬性,獲取所有property的後設資料
    // property的後設資料對應的私有類為_YYModelPropertyMeta,其實就是YYClassPropertyInfo類物件的封裝
    NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
    YYClassInfo *curClassInfo = classInfo;
    while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
        for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
            if (!propertyInfo.name) continue;
            if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
            if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
            _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
                                                                    propertyInfo:propertyInfo
                                                                         generic:genericMapper[propertyInfo.name]];
            if (!meta || !meta->_name) continue;
            if (!meta->_getter || !meta->_setter) continue;
            if (allPropertyMetas[meta->_name]) continue;
            allPropertyMetas[meta->_name] = meta;
        }
        curClassInfo = curClassInfo.superClassInfo;
    }
    if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;
    
    // create mapper
    // 處理自定義的屬性對於json資料key的對映配置
    NSMutableDictionary *mapper = [NSMutableDictionary new];
    NSMutableArray *keyPathPropertyMetas = [NSMutableArray new];
    NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new];
    
    if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
        NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
        [customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
            _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
            if (!propertyMeta) return;
            [allPropertyMetas removeObjectForKey:propertyName];
            
            if ([mappedToKey isKindOfClass:[NSString class]]) {
                if (mappedToKey.length == 0) return;
                
                propertyMeta->_mappedToKey = mappedToKey;
                NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."];
                for (NSString *onePath in keyPath) {
                    if (onePath.length == 0) {
                        NSMutableArray *tmp = keyPath.mutableCopy;
                        [tmp removeObject:@""];
                        keyPath = tmp;
                        break;
                    }
                }
                if (keyPath.count > 1) {
                    propertyMeta->_mappedToKeyPath = keyPath;
                    [keyPathPropertyMetas addObject:propertyMeta];
                }
                // 處理有多個屬性繫結到同一個json資料key的配置
                propertyMeta->_next = mapper[mappedToKey] ?: nil;
                mapper[mappedToKey] = propertyMeta;
                
            } else if ([mappedToKey isKindOfClass:[NSArray class]]) {
                
                NSMutableArray *mappedToKeyArray = [NSMutableArray new];
                for (NSString *oneKey in ((NSArray *)mappedToKey)) {
                    if (![oneKey isKindOfClass:[NSString class]]) continue;
                    if (oneKey.length == 0) continue;
                    
                    NSArray *keyPath = [oneKey componentsSeparatedByString:@"."];
                    if (keyPath.count > 1) {
                        [mappedToKeyArray addObject:keyPath];
                    } else {
                        [mappedToKeyArray addObject:oneKey];
                    }
                    
                    if (!propertyMeta->_mappedToKey) {
                        propertyMeta->_mappedToKey = oneKey;
                        propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
                    }
                }
                if (!propertyMeta->_mappedToKey) return;
                
                propertyMeta->_mappedToKeyArray = mappedToKeyArray;
                [multiKeysPropertyMetas addObject:propertyMeta];
                
                // 處理有多個屬性繫結到同一個json資料key的配置
                propertyMeta->_next = mapper[mappedToKey] ?: nil;
                mapper[mappedToKey] = propertyMeta;
            }
        }];
    }
    
    // 處理預設的屬性的對映配置,屬性在json資料中的key就是屬性的原始名稱
    [allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
        propertyMeta->_mappedToKey = name;
        propertyMeta->_next = mapper[name] ?: nil;
        mapper[name] = propertyMeta;
    }];
    
    // 最終把json資料key和屬性的對映關係儲存起來
    if (mapper.count) _mapper = mapper;
    if (keyPathPropertyMetas) _keyPathPropertyMetas = keyPathPropertyMetas;
    if (multiKeysPropertyMetas) _multiKeysPropertyMetas = multiKeysPropertyMetas;
    
    _classInfo = classInfo;
    _keyMappedCount = _allPropertyMetas.count;
    _nsType = YYClassGetNSType(cls);
    _hasCustomWillTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomWillTransformFromDictionary:)]);
    _hasCustomTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformFromDictionary:)]);
    _hasCustomTransformToDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformToDictionary:)]);
    _hasCustomClassFromDictionary = ([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]);
    
    return self;
}
複製程式碼

模型物件的轉換

模型物件的轉換的步驟時序圖如下:

模型物件的轉換的步驟時序圖
模型物件的轉換的步驟時序圖

入口函式yy_modelSetWithDictionary的功能如下:

  • 1、屬性的對映配置的個數大於json資料元素的個數(if分支),優先處理json資料
    • 1.1、遍歷json資料的key
    • 1.2、根據key從meta->_mapper配置中尋找對映的_YYModelPropertyMeta物件
    • 1.3、從json資料中讀取值,給屬性設定值。
  • 2、json資料元素的個數大於屬性的對映配置的個數(else分支),優先處理屬性屬性
    • 2.1、從modelMeta->_allPropertyMetas讀取屬性配置
    • 2.2、從json資料中讀取值,給屬性設定值

對應的程式碼如下,關鍵的地方有新增了註釋:

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    

    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    if (modelMeta->_keyMappedCount == 0) return NO;
    
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }
    
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic);
    
    // 這裡做這個處理主要是為了優化效能,哪個少優先處理哪個,提高效率
    // 1、屬性的對映配置的個數大於json資料元素的個數(if分支),優先處理json資料
    // 1.1、遍歷json資料的key
    // 1.2、根據key從`meta->_mapper`配置中尋找對映的`_YYModelPropertyMeta`物件
    // 1.3、從json資料中讀取值,給屬性設定值。
    // * 因為`meta->_mapper`儲存的是一對一的對映關係,所以另外需要額外的處理keypath對映和陣列型別的key的對映
    // 2、json資料元素的個數大於屬性的對映配置的個數(else分支),優先處理屬性屬性
    // 2.1、從`modelMeta->_allPropertyMetas`讀取屬性配置
    // 2.2、從json資料中讀取值,給屬性設定值
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        // 處理多個屬性對映到同一個json資料的key
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            // 處理keypath,屬性對應json資料key是多層型別的對映
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        if (modelMeta->_multiKeysPropertyMetas) {
            // 處理陣列型別對映,屬性對應json資料key是多個的對映
            CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
    } else {
        // 處理屬性多json資料的key的一對一對映,這種情況多個屬性對映到同一個json資料的key處理方式和ModelSetWithDictionaryFunction方法的不一樣,不過最終的結果是一樣的
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    if (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}
複製程式碼

以上程式碼中使用到的兩個方法ModelSetWithDictionaryFunctionModelSetWithPropertyMetaArrayFunction,這兩個方法很簡單,使用獲取值方法YYValueForMultiKeysYYValueForKeyPath,根據_YYModelPropertyMeta物件的配置從json資料中獲取值,然後呼叫ModelSetValueForProperty給物件的屬性設定值。

static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
    __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;
    };
}

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);
    }
}
複製程式碼

獲取值方法YYValueForMultiKeysYYValueForKeyPath也比較簡單

  • YYValueForMultiKeys深度遍歷json資料取值
  • YYValueForKeyPath廣度遍歷json資料取值
static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) {
    id value = nil;
    for (NSUInteger i = 0, max = keyPaths.count; i < max; i++) {
        value = dic[keyPaths[i]];
        if (i + 1 < max) {
            if ([value isKindOfClass:[NSDictionary class]]) {
                dic = value;
            } else {
                return nil;
            }
        }
    }
    return value;
}

static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) {
    id value = nil;
    for (NSString *key in multiKeys) {
        if ([key isKindOfClass:[NSString class]]) {
            value = dic[key];
            if (value) break;
        } else {
            value = YYValueForKeyPath(dic, (NSArray *)key);
            if (value) break;
        }
    }
    return value;
}
複製程式碼

到了最關鍵的設定屬性值的方法ModelSetValueForProperty,使用底層的C語言方法objc_msgSend給屬性賦值,該方法區分型別處理值

  • 數值型別
  • NSFoundation型別,包含了NSArray、NSString、NSDictionary、NSData等型別以及對應的可變型別(如果存在)
  • 其餘特殊型別,包含了SEL、block、Struct、Union、C指標、字串指標等

需要注意:

  • 1、可變型別需要特殊處理,需要使用mutableCopy複製一個可變物件賦值給屬性,否則使用可變型別的特有api比如addObject就會出現崩潰;
  • 2、json資料取出的值和property定義的值型別可能不一樣,如果是數值型別,需要把數值轉換為對應型別,否則編譯不過;如果是NSFoundation型別,需要把Json資料轉換為property定義的型別,否則型別會有問題,方法objc_msgSend可以設定成功,比如property中定義的是屬性status_desc是NSString型別,方法objc_msgSend方法的引數傳遞的是一個NSDate型別,使用status_desc的方法比如length就會崩潰,因為型別變為NSDate了,在NSDate中找不到length方法

方法的部分如下:

static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta) {
    if (meta->_isCNumber) {
        NSNumber *num = YYNSNumberCreateFromID(value);
        ModelSetNumberToProperty(model, num, meta);
        if (num) [num class]; // hold the number
    } else if (meta->_nsType) {
        if (value == (id)kCFNull) {
            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
        } else {
            switch (meta->_nsType) {
                case YYEncodingTypeNSString:
                case YYEncodingTypeNSMutableString: {
                    if ([value isKindOfClass:[NSString class]]) {
                        if (meta->_nsType == YYEncodingTypeNSString) {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
                        } else {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
                        }
                    } else if ([value isKindOfClass:[NSNumber class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSNumber *)value).stringValue :
                                                                       ((NSNumber *)value).stringValue.mutableCopy);
                    } else if ([value isKindOfClass:[NSData class]]) {
                        NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding];
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, string);
                    } else if ([value isKindOfClass:[NSURL class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSURL *)value).absoluteString :
                                                                       ((NSURL *)value).absoluteString.mutableCopy);
                    } else if ([value isKindOfClass:[NSAttributedString class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSAttributedString *)value).string :
                                                                       ((NSAttributedString *)value).string.mutableCopy);
                    }
                } break;

// ... 省略很多

  default: break;
            }
        }
    } else {
        BOOL isNull = (value == (id)kCFNull);
        switch (meta->_type & YYEncodingTypeMask) {
            case YYEncodingTypeObject: {
                if (isNull) {
                    ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
                } else if ([value isKindOfClass:meta->_cls] || !meta->_cls) {
                    ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value);
                } else if ([value isKindOfClass:[NSDictionary class]]) {
                    NSObject *one = nil;
                    if (meta->_getter) {
                        one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);
                    }
                    if (one) {
                        [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
                        }
                        one = [cls new];
                        [one yy_modelSetWithDictionary:value];
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one);
                    }
                }
            } break;
                
            case YYEncodingTypeClass: {
                if (isNull) {
                    ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)NULL);
                } else {
                    Class cls = nil;
                    if ([value isKindOfClass:[NSString class]]) {
                        cls = NSClassFromString(value);
                        if (cls) {
                            ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)cls);
                        }
                    } else {
                        cls = object_getClass(value);
                        if (cls) {
                            if (class_isMetaClass(cls)) {
                                ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)value);
                            }
                        }
                    }
                }
            } break;
                
            case  YYEncodingTypeSEL: {
                if (isNull) {
                    ((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)model, meta->_setter, (SEL)NULL);
                } else if ([value isKindOfClass:[NSString class]]) {
                    SEL sel = NSSelectorFromString(value);
                    if (sel) ((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)model, meta->_setter, (SEL)sel);
                }
            } break;
                
            case YYEncodingTypeBlock: {
                if (isNull) {
                    ((void (*)(id, SEL, void (^)()))(void *) objc_msgSend)((id)model, meta->_setter, (void (^)())NULL);
                } else if ([value isKindOfClass:YYNSBlockClass()]) {
                    ((void (*)(id, SEL, void (^)()))(void *) objc_msgSend)((id)model, meta->_setter, (void (^)())value);
                }
            } break;
                
            case YYEncodingTypeStruct:
            case YYEncodingTypeUnion:
            case YYEncodingTypeCArray: {
                if ([value isKindOfClass:[NSValue class]]) {
                    const char *valueType = ((NSValue *)value).objCType;
                    const char *metaType = meta->_info.typeEncoding.UTF8String;
                    if (valueType && metaType && strcmp(valueType, metaType) == 0) {
                        [model setValue:value forKey:meta->_name];
                    }
                }
            } break;
                
            case YYEncodingTypePointer:
            case YYEncodingTypeCString: {
                if (isNull) {
                    ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, (void *)NULL);
                } else if ([value isKindOfClass:[NSValue class]]) {
                    NSValue *nsValue = value;
                    if (nsValue.objCType && strcmp(nsValue.objCType, "^v") == 0) {
                        ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, nsValue.pointerValue);
                    }
                }
            } // break; commented for code coverage in next line
                
            default: break;
        }
    }
}
複製程式碼

其它重要知識點

位操作

YYModel框架中定義了一個列舉YYEncodingType,這個型別的值可以儲存property的值型別(Type Encoding);property的讀寫、原子、記憶體等屬性(Property Encoding);方法的型別(Method Encoding)(暫時沒用到)。在獲取型別方法YYEncodingGetType獲取型別的時候使用位活操作符"|"組合多個Encoding,在設定屬性值的方法比如ModelSetNumberToProperty,使用位與操作符“&”判斷是否是特定的型別。使用這種型別有兩個好處:1、節省空間;2、位操作效率比較高,比如乘除2,使用左移或者右移的效率會比乘除法高,不過實際編碼中為了可讀性還是最好使用乘除法,另外現代的編譯器會幫我們做很多優化,在絕大多數的場景中,我們可以不用太關注這些細枝末節,真正需要優化的時候才考慮做這類程式碼上的優化。

訊號和鎖

YYModel中使用的是訊號鎖dispatch_semaphore_t,為什麼使用這種型別的鎖呢,可以從作者的部落格中找到答案不再安全的 OSSpinLock ,這種鎖的效率還是非常高的,又可以避免OSSpinLock鎖產生的問題。這個場景中dispatch_semaphore_t是一個讀寫互斥鎖,CFDictionaryGetValue是讀操作,CFDictionarySetValue是寫操作,這兩者同一時間只能一個進入,其餘的需要等待。

+ (instancetype)metaWithClass:(Class)cls {
    if (!cls) return nil;
    static CFMutableDictionaryRef cache;
    static dispatch_once_t onceToken;
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
    dispatch_semaphore_signal(lock);
    if (!meta || meta->_classInfo.needUpdate) {
        meta = [[_YYModelMeta alloc] initWithClass:cls];
        if (meta) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
            dispatch_semaphore_signal(lock);
        }
    }
    return meta;
}
複製程式碼

CF框架API

yy_modelSetWithDictionary方法中使用到CF框架中容器遍歷的API有CFDictionaryApplyFunctionCFArrayApplyFunction,一般滴使用CF框架的API效能會高於NSFoundation框架的API,因為NSFoundation框架的API是基於CF框架的API,此時NSFoundation框架相當於一箇中間者,繞了一步。更多的關於CFArray的知識可以參考Exposing NSMutableArray這篇文章的介紹。

相關文章