中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

YYModel 源碼剖析:關(guān)注性能

2018-07-20    來源:編程學(xué)習(xí)網(wǎng)

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

前言

json與模型的轉(zhuǎn)換框架很多,YYModel 一出,性能吊打同類組件,終于找了些時間觀摩了一番,確實收益頗多,寫下此文作為分享。

由于該框架代碼比較多,考慮到突出重點,壓縮篇幅,不會有太多筆墨在基礎(chǔ)知識上,很多展示源碼部分會做刪減,重點是在理解作者思維。讀者需要具備一定的 runtime 知識,若想閱讀起來輕松一些,最好自己打開源碼做參照。

一、框架的核心思路

使用過框架的朋友應(yīng)該很熟悉如下的這些方法:

@interface NSObject (YYModel) + (nullable instancetype)yy_modelWithJSON:(id)json;
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;
- (nullable id)yy_modelToJSONObject;
- (nullable NSData *)yy_modelToJSONData;
......

框架解決的問題,就是實現(xiàn) json 和  OC對象 間的轉(zhuǎn)換,這個過程的核心問題就是  json數(shù)據(jù) 和  OC對象的成員變量 之間的映射關(guān)系。

而這個映射關(guān)系,需要借助 runtime 來完成。只需要傳入一個  Class 類變量,框架內(nèi)部就能通過 runtime 將該類的屬性以及方法查找出來,默認(rèn)是將屬性名作為映射的 key,然后 json 數(shù)據(jù)就能通過這個映射的 key 匹配賦值(通過objc_msgSend)。

若將 OC 對象轉(zhuǎn)換成 json 數(shù)據(jù),只需要逆向處理一下。

框架做的事情說起來是簡單的,不同開源庫實現(xiàn)的細(xì)節(jié)雖然不同,但是它們的核心思路很相似。

二、類型編碼 Type-Encoding

前面筆者提到,可以通過 runtime 獲取到某個類的所有屬性名字,達(dá)成映射。但是考慮到我們的 模型類 往往會定義很多種類型,比如:double、char、NSString、NSDate、SEL 、NSSet 等,所以需要將元數(shù)據(jù) json(或者字典數(shù)據(jù))轉(zhuǎn)換成我們實際需要的類型。

但是,計算機如何知道我們定義的 模型類 的屬性是什么類型的呢?由此,引入類型編碼的概念——

兩個關(guān)于類型編碼的官方文檔:

文檔一

文檔二

Type-Encoding 是指定的一套類型編碼,在使用 runtime 獲取某個類的成員變量、屬性、方法的時候,能同時獲取到它們的類型編碼,通過這個編碼就能辨別這些成員變量、屬性、方法的數(shù)據(jù)類型(也包括屬性修飾符、方法修飾符等)。

枚舉的處理

關(guān)于類型編碼的具體細(xì)節(jié)請自行查閱文檔,本文不做講解。在 YYModel 的源碼中,作者使用了一個枚舉來對應(yīng)不同的類型,見名知意,方便在框架中使用:

typedef NS_OPTIONS(NSUInteger, YYEncodingType) {
    YYEncodingTypeMask       = 0xFF, ///< mask of type value
    YYEncodingTypeUnknown    = 0, ///< unknown
    YYEncodingTypeVoid       = 1, ///< void
    ......
    YYEncodingTypeCArray     = 22, ///< char[10] (for example)
    
    YYEncodingTypeQualifierMask   = 0xFF00,   ///< mask of qualifier
    YYEncodingTypeQualifierConst  = 1 << 8,  ///< const
    YYEncodingTypeQualifierIn     = 1 << 9,  ///< in
    ......
    YYEncodingTypeQualifierOneway = 1 << 14, ///< oneway
    
    YYEncodingTypePropertyMask         = 0xFF0000, ///< mask of property
    YYEncodingTypePropertyReadonly     = 1 << 16, ///< readonly
    YYEncodingTypePropertyCopy         = 1 << 17, ///< copy
    ......
    YYEncodingTypePropertyDynamic      = 1 << 23, ///< @dynamic
};

筆者并不是想把所有類型編碼貼出來看,所以做了省略。這個枚舉可能是多選的,所以使用了NS_OPTIONS而不是NS_ENUM(編碼規(guī)范)。

可以看到該枚舉既包含了單選枚舉值,也包含了多選枚舉值,如何讓它們互不影響?

YYEncodingTypeMask、YYEncodingTypeQualifierMask、YYEncodingTypePropertyMask將枚舉值分為三部分,它們的值轉(zhuǎn)換為二進(jìn)制分別為:

然后,這三部分其他枚舉的值,恰巧分布在這三個 mask 枚舉的值分成的三個區(qū)間。在源碼中,會看到如下代碼:

YYEncodingType type; 
if ((type & YYEncodingTypeMask) == YYEncodingTypeVoid) {...}

通過一個 位與& 運算符,直接將高于YYEncodingTypeMask的值過濾掉,然后實現(xiàn)單值比較。

這是一個代碼技巧,挺有意思。

關(guān)于 Type-Encoding 轉(zhuǎn)換 YYEncodingType 枚舉的代碼就不解釋了,基本上根據(jù)官方文檔來的。

三、將底層數(shù)據(jù)裝進(jìn)中間類

在 YYClassInfo 文件中,可以看到有這么幾個類:

YYClassIvarInfo
YYClassMethodInfo
YYClassPropertyInfo
YYClassInfo

很明顯,他們是將 Ivar、Method、objc_property_t、Class 的相關(guān)信息裝進(jìn)去,這樣做一是方便使用,二是為了做緩存。

在源碼中可以看到:

操作 runtime 底層類型的時候,由于它們不受 ARC 自動管理內(nèi)存,所以記得用完了釋放(但是不要去釋放 const 常量),釋放之前切記判斷該內(nèi)存是否存在防止意外crash。

基本的轉(zhuǎn)換過程很簡單,不一一討論,下面提出一些值得注意的地方:

YYClassPropertyInfo 記錄屬性<> 包裹部分

@implementation YYClassPropertyInfo
- (instancetype)initWithProperty:(objc_property_t)property {
...
    NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
    ...
    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;
...
}
@end

在對 objc_property_t 的轉(zhuǎn)換中,作者查找了類型編碼中用<> 包裹的部分,然后把它們放進(jìn)一個數(shù)組。這是作者做的一個基于用戶體驗的優(yōu)化,比如一個屬性@property NSArray arr,框架就可以解析到 arr 容器內(nèi)部是包含的 CustomObject 類型,而不需使用者專門寫方法去映射(當(dāng)然可以自定義映射)。關(guān)于具體的邏輯后面會講到。

YYClassInfo 結(jié)構(gòu)

@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 *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary *propertyInfos; ///< properties
...

可以看到,Class 類的成員變量、屬性、方法分別裝入了三個 hash 容器(ivarInfos/methodInfos/propertyInfos)。

superClassInfo 指向父類,初始化時框架會循環(huán)向上查找,直至當(dāng)前 Class 的父類不存在(NSObject 父類指針為 nil),這類似一個單向的鏈表,將有繼承關(guān)系的類信息全部串聯(lián)起來。這么做的目的,就是為了 json 轉(zhuǎn)模型的時候,同樣把父類的屬性名作為映射的 key。初始化 YYClassInfo 的代碼大致如下:

- (instancetype)initWithClass:(Class)cls {
    if (!cls) return nil;
    self = [super init];
    ...
//_update方法就是將當(dāng)前類的成員變量列表、屬性列表、方法列表轉(zhuǎn)換放進(jìn)對應(yīng)的 hash
    [self _update];
//獲取父類信息。 classInfoWithClass: 是一個獲取類的方法,里面有緩存機制,下一步會講到
    _superClassInfo = [self.class classInfoWithClass:_superCls];
    return self;
}

YYClassInfo 緩存

作者做了一個類信息(YYClassInfo)緩存的機制:

+ (instancetype)classInfoWithClass:(Class)cls {
    if (!cls) return nil;
//初始化幾個容器和鎖
    static CFMutableDictionaryRef classCache;
    static CFMutableDictionaryRef metaCache;
    static dispatch_once_t onceToken;
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
//讀取緩存
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
//更新成員變量列表、屬性列表、方法列表
    if (info && info->_needUpdate) [info _update];
    dispatch_semaphore_signal(lock);
//若無緩存,將 Class 類信息轉(zhuǎn)換為新的 YYClassInfo 實例,并且放入緩存
    if (!info) {
        info = [[YYClassInfo alloc] initWithClass:cls];
        if (info) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
            dispatch_semaphore_signal(lock);
        }
    }
    return info;
}

由于同一個類的相關(guān)信息在程序運行期間通常是相同的,所以使用 classCache(類hash) 和 metaCache(元類hash) 緩存已經(jīng)通過 runtime 轉(zhuǎn)換為 YYClassInfo 的 Class,保證不會重復(fù)轉(zhuǎn)換 Class 類信息做無用功;考慮到 runtime 帶來的動態(tài)特性,作者使用了一個 bool 值判斷是否需要更新成員變量列表、屬性列表、方法列表,_update方法就是重新獲取這些信息。

這個緩存機制能帶來很高的效率提升,是 YYModel 一個比較核心的操作。

有幾個值得注意和學(xué)習(xí)的地方:

  1. 使用 static 修飾局部變量提升其生命周期,而又不改變其作用域,保證在程序運行期間局部變量不會釋放,又防止了其他代碼對該局部變量的訪問。

  2. 線程安全的考慮。在初始化 static 變量的時候,使用dispatch_once()保證線程安全;在讀取和寫入使用dispatch_semaphore_t信號量保證線程安全。

四、一些工具方法

在進(jìn)入核心業(yè)務(wù)之前,先介紹一些 NSObject+YYModel.m 里面值得注意的工具方法。

在工具方法中,經(jīng)常會看到這么一個宏來修飾函數(shù):

#define force_inline __inline__ __attribute__((always_inline))

它的作用是強制內(nèi)聯(lián),因為使用 inline 關(guān)鍵字最終會不會內(nèi)聯(lián)還是由編譯器決定。對于這些強制內(nèi)聯(lián)的函數(shù)參數(shù),作者經(jīng)常使用 __unsafe_unretained 來修飾,拒絕其引用計數(shù)+1,以減少內(nèi)存開銷。

將 id 類型轉(zhuǎn)換為 NSNumber

static force_inline NSNumber *YYNSNumberCreateFromID(__unsafe_unretained id value) {
    static NSCharacterSet *dot;
    static NSDictionary *dic;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dot = [NSCharacterSet characterSetWithRange:NSMakeRange('.', 1)];
        dic = @{@"TRUE" :   @(YES),
                @"True" :   @(YES),
                @"true" :   @(YES),
                ...
                @"NIL" :    (id)kCFNull,
                @"Nil" :    (id)kCFNull,
                ...
    });
    
    if (!value || value == (id)kCFNull) return nil;
    if ([value isKindOfClass:[NSNumber class]]) return value;
    if ([value isKindOfClass:[NSString class]]) {
        NSNumber *num = dic[value];
        if (num) {
            if (num == (id)kCFNull) return nil;
            return num;
        }
        ...
    return nil;
}

這里的轉(zhuǎn)換處理的主要是 NSString 到 NSNumber 的轉(zhuǎn)換,由于服務(wù)端返回給前端的 bool 類型、空類型多種多樣,這里使用了一個 hash 將所有的情況作為 key 。然后轉(zhuǎn)換的時候直接從 hash 中取值,將查找效率最大化提高。

NSString 轉(zhuǎn)換為 NSDate

static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string) {
    typedef NSDate* (^YYNSDateParseBlock)(NSString *string);
    #define kParserNum 34
    static YYNSDateParseBlock blocks[kParserNum + 1] = {0};
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ...
        { /*
             Fri Sep 04 00:12:21 +0800 2015 // Weibo, Twitter
             Fri Sep 04 00:12:21.000 +0800 2015
             */
            NSDateFormatter *formatter = [NSDateFormatter new];
            formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
            formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";
            NSDateFormatter *formatter2 = [NSDateFormatter new];
            formatter2.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
            formatter2.dateFormat = @"EEE MMM dd HH:mm:ss.SSS Z yyyy";
            blocks[30] = ^(NSString *string) { return [formatter dateFromString:string]; };
            blocks[34] = ^(NSString *string) { return [formatter2 dateFromString:string]; };
        }
    });
    if (!string) return nil;
    if (string.length > kParserNum) return nil;
    YYNSDateParseBlock parser = blocks[string.length];
    if (!parser) return nil;
    return parser(string);
    #undef kParserNum
}

在 NSDictionary 原數(shù)據(jù)轉(zhuǎn)模型的時候,會有將時間格式編碼的字符串原數(shù)據(jù)轉(zhuǎn)成 NSDate 類型的需求。

此處作者有個巧妙的設(shè)計 —— blocks。它是一個長度為 kParserNum + 1 的數(shù)組,里面的元素是YYNSDateParseBlock 類型的閉包。

作者將幾乎所有(此處代碼有刪減)的關(guān)于時間的字符串格式羅列出來,創(chuàng)建等量 NSDateFormatter 對象和閉包對象,然后將 NSDateFormatter 對象 放入閉包對象的代碼塊中返回轉(zhuǎn)換好的 NSDate 類型,最后將閉包對象放入數(shù)組,而 放入的下標(biāo)即為字符串的長度

實際上這也是 hash 思想,當(dāng)傳入有效時間格式的 NSString 對象時,通過其長度就能直接取到 blocks 數(shù)組中的閉包對象,調(diào)用閉包傳入該字符串就能直接得到轉(zhuǎn)換后的 NSDate 對象。

最后使用#undef解除 kParserNum 宏定義,避免外部的宏沖突。

獲取 NSBlock 類

static force_inline Class YYNSBlockClass() {
    static Class cls;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        void (^block)(void) = ^{};
        cls = ((NSObject *)block).class;
        while (class_getSuperclass(cls) != [NSObject class]) {
            cls = class_getSuperclass(cls);
        }
    });
    return cls; // current is "NSBlock"
}

NSBlock 是 OC 中閉包的隱藏跟類(繼承自 NSObject),先將一個閉包強轉(zhuǎn)為 NSObject 獲取其 Class 類型,然后循環(huán)查找父類,直到該 Class 的父類為 NSObject.class。

五、輔助類 _YYModelPropertyMeta

位于 NSObject+YYModel.m 中的輔助類 _YYModelPropertyMeta 是基于之前提到的 YYClassPropertyInfo 的二次解析封裝,結(jié)合屬性歸屬類添加了很多成員變量來輔助完成框架的核心業(yè)務(wù)功能,先來看一下它的結(jié)構(gòu):

@interface _YYModelPropertyMeta : NSObject {
    @package
    NSString *_name;             ///< property's name
    YYEncodingType _type;        ///< property's type
    YYEncodingNSType _nsType;    ///< property's Foundation type
    BOOL _isCNumber;             ///< is c number type
    Class _cls;                  ///< property's class, or nil
    Class _genericCls;           ///< container's generic class, or nil if threr's no generic class
    SEL _getter;                 ///< getter, or nil if the instances cannot respond
    SEL _setter;                 ///< setter, or nil if the instances cannot respond
    BOOL _isKVCCompatible;       ///< YES if it can access with key-value coding
    BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver
    BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary:
    
    NSString *_mappedToKey;      ///< the key mapped to
    NSArray *_mappedToKeyPath;   ///< the key path mapped to (nil if the name is not key path)
    NSArray *_mappedToKeyArray;  ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
    YYClassPropertyInfo *_info;  ///< property's info
    _YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key.
}
@end

結(jié)合注釋可以看明白一部分的變量的含義,個別成員變量的作用需要結(jié)合另外一個輔助類 _YYModelMeta 來解析,后面再討論。

_isStructAvailableForKeyedArchiver: 標(biāo)識如果該屬性是結(jié)構(gòu)體,是否支持編碼,支持編碼的結(jié)構(gòu)體可以在源碼里面去看。

_isKVCCompatible: 標(biāo)識該成員變量是否支持 KVC。

在該類的初始化方法中,有如下處理:

@implementation _YYModelPropertyMeta
+ (instancetype)metaWithClassInfo:(YYClassInfo *)classInfo propertyInfo:(YYClassPropertyInfo *)propertyInfo generic:(Class)generic {
    // support pseudo generic class with protocol name
    if (!generic && propertyInfo.protocols) {
        for (NSString *protocol in propertyInfo.protocols) {
            Class cls = objc_getClass(protocol.UTF8String);
            if (cls) {
                generic = cls;
                break;
            }
        }
    }
...

generic 即為容器的元素類型,若初始化時沒有傳入,這里會讀取屬性<> 包含的字符,轉(zhuǎn)換成對應(yīng)的類。

六、輔助類 _YYModelMeta

_YYModelMeta 是核心輔助類:

@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo;
    /// Key:mapped key and key path, Value:_YYModelPropertyMeta.
    NSDictionary *_mapper;
    /// Array, all property meta of this model.
    NSArray *_allPropertyMetas;
    /// Array, property meta which is mapped to a key path.
    NSArray *_keyPathPropertyMetas;
    /// Array, property meta which is mapped to multi keys.
    NSArray *_multiKeysPropertyMetas;
    /// The number of mapped key (and key path), same to _mapper.count.
    NSUInteger _keyMappedCount;
    /// Model class type.
    YYEncodingNSType _nsType;
    
    BOOL _hasCustomWillTransformFromDictionary;
    BOOL _hasCustomTransformFromDictionary;
    BOOL _hasCustomTransformToDictionary;
    BOOL _hasCustomClassFromDictionary;
}
@end

_classInfo記錄的 Class 信息;_mapper/_allPropertyMetas是記錄屬性信息(_YYModelPropertyMeta)的 hash 和數(shù)組;_keyPathPropertyMetas/_multiKeysPropertyMetas是記錄屬性映射為路徑和映射為多個 key 的數(shù)組;_nsType記錄當(dāng)前模型的類型;最后四個 bool 記錄是否有自定義的相關(guān)實現(xiàn)。

下面將 _YYModelMeta 類初始化方法分塊講解(建議打開源碼對照)。

黑名單/白名單

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
    // Get black list
    NSSet *blacklist = nil;
    if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
        NSArray *properties = [(id)cls modelPropertyBlacklist];
        if (properties) {
            blacklist = [NSSet setWithArray:properties];
        }
    }
    // Get white list
    NSSet *whitelist = nil;
    if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
        NSArray *properties = [(id)cls modelPropertyWhitelist];
        if (properties) {
            whitelist = [NSSet setWithArray:properties];
        }
    }
...

YYModel 是包含了眾多自定義方法的協(xié)議,modelPropertyBlacklist和modelPropertyWhitelist分別為黑名單和白名單協(xié)議方法。

自定義容器元素類型

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
// Get container property's generic class
    NSDictionary *genericMapper = nil;
    if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
        genericMapper = [(id)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;
        }
    }
...

同樣是 YYModel 協(xié)議下的方法:modelContainerPropertyGenericClass,返回了一個自定義的容器與內(nèi)部元素的 hash。比如模型中一個容器屬性@property NSArray *arr;,當(dāng)你希望轉(zhuǎn)換過后它內(nèi)部裝有CustomObject類型時,你需要實現(xiàn)該協(xié)議方法,返回{@"arr":@"CustomObject"}或者@{@"arr": CustomObject.class}(看上面代碼可知作者做了兼容)。

當(dāng)然,你可以指定模型容器屬性的元素,如:@property NSArray *arr;,若你未在上述協(xié)議中返回該屬性的映射關(guān)系,那么在將該屬性轉(zhuǎn)換成中間類 _YYModelPropertyMeta 時,會自動查找屬性的  type-ecoding  中的<> 的包裹部分,從而定位你的容器里面是什么元素。(可以查看前面對 _YYModelPropertyMeta 初始化方法的解析)

查找該類的所有屬性

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
    YYClassInfo *curClassInfo = classInfo;
//循環(huán)查找父類屬性,但是忽略跟類 (NSObject/NSProxy)
    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;
//將屬性轉(zhuǎn)換為中間類
            _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
                                                                    propertyInfo:propertyInfo
                                                                         generic:genericMapper[propertyInfo.name]];
            ...
//記錄
            allPropertyMetas[meta->_name] = meta;
        }
//指針向父類推進(jìn)
        curClassInfo = curClassInfo.superClassInfo;
    }
...

自定義映射關(guān)系

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
    if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
        NSDictionary *customMapper = [(id )cls modelCustomPropertyMapper];
//遍歷自定義映射的 hash
        [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;
                //1、判斷是否是路徑
                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];
                }
                //2、連接相同映射的屬性
                propertyMeta->_next = mapper[mappedToKey] ?: nil;
                mapper[mappedToKey] = propertyMeta;
                
            } else if ([mappedToKey isKindOfClass:[NSArray class]]) {
                ...
            }
        }];
    }
...

modelCustomPropertyMapper協(xié)議方法是用于自定義映射關(guān)系,比如需要將 json 中的 id 字段轉(zhuǎn)換成屬性:@property NSString *ID;,由于系統(tǒng)是默認(rèn)將屬性的名字作為映射的依據(jù),所以這種業(yè)務(wù)場景需要使用者自行定義映射關(guān)系。

在實現(xiàn)映射關(guān)系協(xié)議時,有多種寫法:

+ (NSDictionary *)modelCustomPropertyMapper {
         return @{@"name"  : @"n",
                  @"page"  : @"p",
                  @"desc"  : @"ext.desc",
                  @"bookID": @[@"id", @"ID", @"book_id"]};
}

key 是模型中的屬性名字,value 就是對于 json(或字典)數(shù)據(jù)源的字段。特別的,可以使用“.”來鏈接字符形成一個路徑,也可以傳入一個數(shù)組,當(dāng)映射的是一個數(shù)組的時候,json -> model 的時候會找到第一個有效的映射作為model屬性的值。比如上面代碼中,在數(shù)據(jù)源中找到ID字符,便會將其值給當(dāng)前模型類的bookID屬性,忽略掉后面的映射(book_id)。

性能層面,可以在代碼中看到兩個閃光點:

1、判斷是否是路徑

將映射的value拆分成keyPath數(shù)組,然后做了一個遍歷,當(dāng)遍歷到@""空字符值時,深拷貝一份keyPath移除所有的@""然后break。

這個操作看似簡單,實則是作者對性能的優(yōu)化。通常情況下,傳入的路徑是正確的a.b.c,這時不需要移除@""。而當(dāng)路徑錯誤,比如a..b.c、a.b.c.時,分離字符串時keyPath中就會有空值@""。由于componentsSeparatedByString方法返回的是一個不可變的數(shù)組,所以移除keyPath中的@""需要先深拷貝一份可變內(nèi)存。

作者此處的想法很明顯:在正常情況下,不需要移除,也就是不需要深拷貝keyPath增加內(nèi)存開銷。

若考慮到極致的性能,會發(fā)現(xiàn)此處做了兩個遍歷(一個拆分mappedToKey的遍歷,一個keyPath的遍歷),應(yīng)該一個遍歷就能做出來,有興趣的朋友可能嘗試一下。

不過此處的路徑不會很長,也就基本可以忽略掉多的這幾次遍歷了。

2、連接相同映射的屬性

之前解析 _YYModelPropertyMeta 類時,可以發(fā)現(xiàn)它有個成員變量_YYModelPropertyMeta *_next;,它的作用就可以從此處看出端倪。

代碼中,mapper是記錄的所有屬性的 hash(由前面未貼出代碼得到),hash 的 key 即為映射的值(路徑)。作者做了一個判斷,若mapper中存在相同 key 的屬性,就改變了一下指針,做了一個鏈接,將相同映射 key 的屬性連接起來形成一個鏈表。

這么做的目的很簡單,就是為了在 json 數(shù)據(jù)源查找到某個目標(biāo)值時,可以移動_next指針,將所有的相同映射的屬性統(tǒng)統(tǒng)賦值,從而達(dá)到不重復(fù)查找數(shù)據(jù)源相同路徑值的目的。

對象緩存

+ (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;
}

_YYModelMeta 的緩存邏輯和 上文中 YYClassInfo 的緩存邏輯一樣,不多闡述。

七、給數(shù)據(jù)模型屬性賦值 / 將數(shù)據(jù)模型解析成 json

實際上上文已經(jīng)將 YYModel 的大部分內(nèi)容講解完了,可以說之前的都是準(zhǔn)備工作。

NSObject+YYModel.m 中有個很長的方法:

static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta) {...}

看該方法的名字應(yīng)該很容易猜到,這就是將數(shù)據(jù)模型(model)中的某個屬性(meta)賦值為目標(biāo)值(value)。具體代碼不貼了,主要是根據(jù)之前的一些輔助的類,利用objc_msgSend給目標(biāo)數(shù)據(jù) model 發(fā)送屬性的 setter 方法。代碼看起來復(fù)雜,實際上很簡單。

相反地,有這樣一個方法將已經(jīng)賦值的數(shù)據(jù)模型解析成 json:

static id ModelToJSONObjectRecursive(NSObject *model) {...}

實現(xiàn)都是根據(jù)前文解析的那些中間類來處理的。

性能的優(yōu)化

直接使用objc_msgSend給對象發(fā)送消息的效率要高于使用 KVC,可以在源碼中看到作者但凡可以使用發(fā)送消息賦值處理的,都不會使用 KVC。

八、從入口函數(shù)說起

回到開頭,有幾個方法是經(jīng)常使用的(當(dāng)然包括 NSArray 和 NSDictionary 中的延展方法):

+ (nullable instancetype)yy_modelWithJSON:(id)json;
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;

這些方法其實落腳點都在一個方法:

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
//通過 Class 獲取 _YYModelMeta 實例
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    ...
 /*使用 ModelSetContext 結(jié)構(gòu)體將以下內(nèi)容裝起來:
1、具體模型對象(self)  
2、通過模型對象的類 Class 轉(zhuǎn)換的 _YYModelMeta 對象(modelMeta)
3、json 轉(zhuǎn)換的原始數(shù)據(jù)(dic)
*/
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic);
    
//執(zhí)行轉(zhuǎn)換
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        if (modelMeta->_multiKeysPropertyMetas) {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
    } else {
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    ...
    return YES;
}

這里使用 CF 框架下的函數(shù)是為提升執(zhí)行效率。

至于ModelSetWithPropertyMetaArrayFunction和ModelSetWithDictionaryFunction的實現(xiàn)不復(fù)雜,不多解析。

九、組件對外提供的一些工具方法

作者很細(xì)心的提供了一些工具方法方便開發(fā)者使用。

拷貝

- (id)yy_modelCopy;

注意是深拷貝。

歸檔/解檔

- (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;
- (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;

喜歡用歸解檔朋友的福音。

hash 值

- (NSUInteger)yy_modelHash;

提供了一個現(xiàn)成的 hash 表算法,方便開發(fā)者構(gòu)建 hash 數(shù)據(jù)結(jié)構(gòu)。

判斷相等

- (BOOL)yy_modelIsEqual:(id)model;

在方法實現(xiàn)中,當(dāng)兩個待比較對象的 hash 值不同時,作者使用if ([self hash] != [model hash]) return NO;判斷來及時返回,提高比較效率。

后語

本文主要是剖析 YYModel 的重點、難點、閃光點,更多的技術(shù)實現(xiàn)細(xì)節(jié)請查閱源碼,作者的細(xì)節(jié)處理得很棒。

從該框架中,可以看到作者對性能的極致追求,這也是作為一位合格的開發(fā)者應(yīng)有的精神。不斷的探究實踐思考,才能真正的做好一件事。

希望本文能讓讀者朋友對 YYModel 有更深的理解

參考文獻(xiàn):作者 ibireme 的博客   iOS JSON 模型轉(zhuǎn)換庫評測

作者:indulge_in

來自:https://www.jianshu.com/p/fe30e6bbc551

 

 

標(biāo)簽: isp 安全 代碼 開發(fā)者 評測

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:一遍記住Java常用的八種排序算法與代碼實現(xiàn)

下一篇:Java并發(fā)編程-volatile