YYModel 源碼剖析:關注性能

YYKit 系列源碼剖析文章:html

前言

json與模型的轉換框架不少,YYModel 一出,性能吊打同類組件,終於找了些時間觀摩了一番,確實收益頗多,寫下此文做爲分享。前端

因爲該框架代碼比較多,考慮到突出重點,壓縮篇幅,不會有太多筆墨在基礎知識上,不少展現源碼部分會作刪減,重點是在理解做者思惟。讀者須要具有必定的 runtime 知識,若想閱讀起來輕鬆一些,最好本身打開源碼作參照。ios

源碼基於 1.0.4 版本。算法

1、框架的核心思路

使用過框架的朋友應該很熟悉以下的這些方法:json

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

框架解決的問題,就是實現 jsonOC對象 間的轉換,這個過程的核心問題就是 json數據OC對象的成員變量 之間的映射關係。數組

而這個映射關係,須要藉助 runtime 來完成。只須要傳入一個 Class 類變量,框架內部就能經過 runtime 將該類的屬性以及方法查找出來,默認是將屬性名做爲映射的 key,而後 json 數據就能經過這個映射的 key 匹配賦值(經過 objc_msgSend)。緩存

若將 OC 對象轉換成 json 數據,只須要逆向處理一下。安全

框架作的事情提及來是簡單的,不一樣開源庫實現的細節雖然不一樣,可是它們的核心思路很類似。bash

2、類型編碼 Type-Encoding

前面筆者提到,能夠經過 runtime 獲取到某個類的全部屬性名字,達成映射。可是考慮到咱們的 模型類 每每會定義不少種類型,好比:double、char、NSString、NSDate、SEL 、NSSet 等,因此須要將元數據 json(或者字典數據)轉換成咱們實際須要的類型。數據結構

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

兩個關於類型編碼的官方文檔: 文檔一 文檔二

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

枚舉的處理

關於類型編碼的具體細節請自行查閱文檔,本文不作講解。在 YYModel 的源碼中,做者使用了一個枚舉來對應不一樣的類型,見名知意,方便在框架中使用:

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(編碼規範)。

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

做者經過YYEncodingTypeMask、YYEncodingTypeQualifierMask、YYEncodingTypePropertyMask 三個掩碼將枚舉值分爲三部分,它們的值轉換爲二進制分別爲:

0000 0000 0000 0000 1111 1111
0000 0000 1111 1111 0000 0000
1111 1111 0000 0000 0000 0000
複製代碼

而後,這三部分其餘枚舉的值,恰巧分佈在這三個 mask 枚舉的值分紅的三個區間。在源碼中,會看到以下代碼:

YYEncodingType type;
if ((type & YYEncodingTypeMask) == YYEncodingTypeVoid) {...}
複製代碼

經過一個 位與& 運算符,直接將高於 YYEncodingTypeMask 的值過濾掉,而後實現單值比較。

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

關於 Type-Encoding 轉換 YYEncodingType 枚舉的代碼就不解釋了,基本上根據官方文檔來的。

3、將底層數據裝進中間類

在 YYClassInfo 文件中,能夠看到有這麼幾個類:

YYClassIvarInfo
YYClassMethodInfo
YYClassPropertyInfo
YYClassInfo
複製代碼

很明顯,他們是將 Ivar、Method、objc_property_t、Class 的相關信息裝進去,這樣作一是方便使用,二是爲了作緩存。

在源碼中能夠看到: 操做 runtime 底層類型的時候,因爲它們不受 ARC 自動管理內存,因此記得用完了釋放(可是不要去釋放 const 常量),釋放以前切記判斷該內存是否存在防止意外crash。

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

屬性協議的緩存

@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;
...
}
...
複製代碼

這裏做者將屬性的協議一樣存儲起來,在後文會描述這些協議的做用。

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
...
複製代碼

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

superClassInfo 指向父類,初始化時框架會循環向上查找,直至當前 Class 的父類不存在(NSObject 父類指針爲 nil),這相似一個單向的鏈表,將有繼承關係的類信息所有串聯起來。這麼作的目的,就是爲了 json 轉模型的時候,一樣把父類的屬性名做爲映射的 key。初始化 YYClassInfo 的代碼大體以下:

- (instancetype)initWithClass:(Class)cls {
    if (!cls) return nil;
    self = [super init];
    ...
//_update方法就是將當前類的成員變量列表、屬性列表、方法列表轉換放進對應的 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 類信息轉換爲新的 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;
}
複製代碼

因爲同一個類的相關信息在程序運行期間一般是相同的,因此使用 classCache(類hash) 和 metaCache(元類hash) 緩存已經經過 runtime 轉換爲 YYClassInfo 的 Class,保證不會重複轉換 Class 類信息作無用功;考慮到 runtime 帶來的動態特性,做者使用了一個 bool 值判斷是否須要更新成員變量列表、屬性列表、方法列表,_update方法就是從新獲取這些信息。

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

有幾個值得注意和學習的地方:

  1. 使用 static 修飾局部變量提高其生命週期,而又不改變其做用域,保證在程序運行期間局部變量不會釋放,又防止了其餘代碼對該局部變量的訪問。
  2. 線程安全的考慮。在初始化 static 變量的時候,使用dispatch_once()保證線程安全;在讀取和寫入使用 dispatch_semaphore_t信號量保證線程安全。

4、一些工具方法

在進入核心業務以前,先介紹一些 NSObject+YYModel.m 裏面值得注意的工具方法。

在工具方法中,常常會看到這麼一個宏來修飾函數:

#define force_inline __inline__ __attribute__((always_inline))
複製代碼

它的做用是強制內聯,由於使用 inline 關鍵字最終會不會內聯仍是由編譯器決定。對於這些強制內聯的函數參數,做者常用 __unsafe_unretained 來修飾,拒絕其引用計數+1,以減小內存開銷。

將 id 類型轉換爲 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;
}
複製代碼

這裏的轉換處理的主要是 NSString 到 NSNumber 的轉換,因爲服務端返回給前端的 bool 類型、空類型多種多樣,這裏使用了一個 hash 將全部的狀況做爲 key 。而後轉換的時候直接從 hash 中取值,將查找效率最大化提升。

NSString 轉換爲 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 原數據轉模型的時候,會有將時間格式編碼的字符串原數據轉成 NSDate 類型的需求。

此處做者有個巧妙的設計 —— blocks。它是一個長度爲 kParserNum + 1 的數組,裏面的元素是YYNSDateParseBlock 類型的閉包。

做者將幾乎全部(此處代碼有刪減)的關於時間的字符串格式羅列出來,建立等量 NSDateFormatter 對象和閉包對象,而後將 NSDateFormatter 對象 放入閉包對象的代碼塊中返回轉換好的 NSDate 類型,最後將閉包對象放入數組,而放入的下標即爲字符串的長度

實際上這也是 hash 思想,當傳入有效時間格式的 NSString 對象時,經過其長度就能直接取到 blocks 數組中的閉包對象,調用閉包傳入該字符串就能直接獲得轉換後的 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),先將一個閉包強轉爲 NSObject 獲取其 Class 類型,而後循環查找父類,直到該 Class 的父類爲 NSObject.class。

5、輔助類 _YYModelPropertyMeta

位於 NSObject+YYModel.m 中的輔助類 _YYModelPropertyMeta 是基於以前提到的 YYClassPropertyInfo 的二次解析封裝,結合屬性歸屬類添加了不少成員變量來輔助完成框架的核心業務功能,先來看一下它的結構:

@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 複製代碼

結合註釋能夠看明白一部分的變量的含義,個別成員變量的做用須要結合另一個輔助類 _YYModelMeta 來解析,後面再討論。

_isStructAvailableForKeyedArchiver: 標識若是該屬性是結構體,是否支持編碼,支持編碼的結構體能夠在源碼裏面去看。 _isKVCCompatible: 標識該成員變量是否支持 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;
            }
        }
    }
...
複製代碼

propertyInfo.protocols即爲以前緩存的屬性的協議名,做者此處嘗試將協議名轉換爲類,若轉換成功,則說明該容器類型屬性的元素類型是該協議同名的類。

這個操做看似意義不大,倒是一個避免轉換過程出錯的優化(雖然這個優化有一些爭議),看以下代碼:

@protocol ModelA <NSObject>
@end

@interface ModelA : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation ModelA
@end

@interface ModelB : NSObject
@property (nonatomic, copy) NSArray<ModelA> *sub;
@end
@implementation ModelB
@end

//字典轉模型
NSDictionary *dataDic = @{@"sub":@[@{@"name":@"a"}, @{@"name":@"b"}]};
ModelB *model = [ModelB yy_modelWithDictionary:dataDic];
複製代碼

你沒有看錯,如此仍然能轉換成功,儘管這句代碼中@property (nonatomic, copy) NSArray<ModelA> *sub;NSArray<>中是協議ModelA,而不是指針類型ModelA *

實際上這就是做者想達到的目的。當業務代碼中有同名的 協議模型,在寫容器的元素類型時(NSArray),開發者有可能會寫錯,而 YYModel 強行糾正了你的錯誤代碼。

嗯。。其實筆者不是很同意這種作法,這會讓後來者包括開發者都懵逼(若是他不瞭解 YYModel 的實現的話)。

6、輔助類 _YYModelMeta

_YYModelMeta 是核心輔助類:

@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo;
    /// Key:mapped key and key path, Value:_YYModelPropertyMeta.
    NSDictionary *_mapper;
    /// Array<_YYModelPropertyMeta>, all property meta of this model.
    NSArray *_allPropertyMetas;
    /// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
    NSArray *_keyPathPropertyMetas;
    /// Array<_YYModelPropertyMeta>, 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 和數組;_keyPathPropertyMetas/_multiKeysPropertyMetas是記錄屬性映射爲路徑和映射爲多個 key 的數組;_nsType 記錄當前模型的類型;最後四個 bool 記錄是否有自定義的相關實現。

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

黑名單/白名單

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
    // 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];
        }
    }
...
複製代碼

YYModel 是包含了衆多自定義方法的協議,modelPropertyBlacklistmodelPropertyWhitelist 分別爲黑名單和白名單協議方法。

自定義容器元素類型

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
// 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; } } ... 複製代碼

一樣是 YYModel 協議下的方法:modelContainerPropertyGenericClass,返回了一個自定義的容器與內部元素的 hash。好比模型中一個容器屬性 @property NSArray *arr;,當你但願轉換事後它內部裝有CustomObject類型時,你須要實現該協議方法,返回 {@"arr":@"CustomObject"} 或者 @{@"arr": CustomObject.class}(看上面代碼可知做者作了兼容)。

固然,你能夠指定模型容器屬性的元素,如:@property NSArray<CustomObject *> *arr;

查找該類的全部屬性

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
    YYClassInfo *curClassInfo = classInfo;
//循環查找父類屬性,可是忽略跟類 (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;
//將屬性轉換爲中間類
            _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
                                                                    propertyInfo:propertyInfo
                                                                         generic:genericMapper[propertyInfo.name]];
            ...
//記錄
            allPropertyMetas[meta->_name] = meta;
        }
//指針向父類推動
        curClassInfo = curClassInfo.superClassInfo;
    }
...
複製代碼

自定義映射關係

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
    if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
        NSDictionary *customMapper = [(id <YYModel>)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;
                //一、判斷是不是路徑
                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];
                }
                //二、鏈接相同映射的屬性
                propertyMeta->_next = mapper[mappedToKey] ?: nil;
                mapper[mappedToKey] = propertyMeta;
                
            } else if ([mappedToKey isKindOfClass:[NSArray class]]) {
                ...
            }
        }];
    }
...
複製代碼

modelCustomPropertyMapper 協議方法是用於自定義映射關係,好比須要將 json 中的 id 字段轉換成屬性:@property NSString *ID;,因爲系統是默認將屬性的名字做爲映射的依據,因此這種業務場景須要使用者自行定義映射關係。

在實現映射關係協議時,有多種寫法:

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

key 是模型中的屬性名字,value 就是對於 json(或字典)數據源的字段。特別的,可使用「.」來連接字符造成一個路徑,也能夠傳入一個數組,當映射的是一個數組的時候,json -> model 的時候會找到第一個有效的映射做爲model屬性的值。好比上面代碼中,在數據源中找到 ID 字符,便會將其值給當前模型類的 bookID 屬性,忽略掉後面的映射(book_id)。

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

一、判斷是不是路徑

將映射的 value 拆分紅 keyPath 數組,而後作了一個遍歷,當遍歷到 @"" 空字符值時,深拷貝一份 keyPath 移除全部的 @"" 而後 break

這個操做看似簡單,實則是做者對性能的優化。一般狀況下,傳入的路徑是正確的 a.b.c,這時不須要移除 @"" 。而當路徑錯誤,好比 a..b.ca.b.c. 時,分離字符串時 keyPath 中就會有空值 @""。因爲 componentsSeparatedByString 方法返回的是一個不可變的數組,因此移除 keyPath 中的 @"" 須要先深拷貝一份可變內存。

做者此處的想法很明顯:在正常狀況下,不須要移除,也就是不須要深拷貝 keyPath 增長內存開銷。

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

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

二、鏈接相同映射的屬性

以前解析 _YYModelPropertyMeta 類時,能夠發現它有個成員變量 _YYModelPropertyMeta *_next;,它的做用就能夠今後處看出端倪。

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

這麼作的目的很簡單,就是爲了在 json 數據源查找到某個目標值時,能夠移動 _next 指針,將全部的相同映射的屬性通通賦值,從而達到不重複查找數據源相同路徑值的目的。

對象緩存

+ (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 的緩存邏輯同樣,很少闡述。

7、給數據模型屬性賦值 / 將數據模型解析成 json

實際上上文已經將 YYModel 的大部份內容講解完了,能夠說以前的都是準備工做。

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

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

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

相反地,有這樣一個方法將已經賦值的數據模型解析成 json:

static id ModelToJSONObjectRecursive(NSObject *model) {...}
複製代碼

實現都是根據前文解析的那些中間類來處理的。

性能的優化

直接使用 objc_msgSend給對象發送消息的效率要高於使用 KVC,能夠在源碼中看到做者但凡可使用發送消息賦值處理的,都不會使用 KVC。

8、從入口函數提及

回到開頭,有幾個方法是常用的(固然包括 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 結構體將如下內容裝起來:
一、具體模型對象(self)  
二、經過模型對象的類 Class 轉換的 _YYModelMeta 對象(modelMeta)
三、json 轉換的原始數據(dic)
*/
    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((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 框架下的函數是爲提高執行效率。

至於 ModelSetWithPropertyMetaArrayFunctionModelSetWithDictionaryFunction 的實現不復雜,很少解析。

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

做者很細心的提供了一些工具方法方便開發者使用。

拷貝
- (id)yy_modelCopy;
複製代碼

注意是深拷貝。

歸檔/解檔
- (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;
- (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;
複製代碼

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

hash 值
- (NSUInteger)yy_modelHash;
複製代碼

提供了一個現成的 hash 表算法,方便開發者構建 hash 數據結構。

判斷相等
- (BOOL)yy_modelIsEqual:(id)model;
複製代碼

在方法實現中,當兩個待比較對象的 hash 值不一樣時,做者使用 if ([self hash] != [model hash]) return NO; 判斷來及時返回,提升比較效率。

後語

本文主要是剖析 YYModel 的重點、難點、閃光點,更多的技術實現細節請查閱源碼,做者的細節處理得很棒。

從該框架中,能夠看到做者對性能的極致追求,這也是做爲一位合格的開發者應有的精神。不斷的探究實踐思考,才能真正的作好一件事。

但願本文能讓讀者朋友對 YYModel 有更深的理解😁。

參考文獻:做者 ibireme 的博客 iOS JSON 模型轉換庫評測

相關文章
相關標籤/搜索