YYModel 原始碼剖析:關注效能

波兒菜發表於2019-07-30

YYKit 系列原始碼剖析文章:

前言

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

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

原始碼基於 1.0.4 版本。

一、框架的核心思路

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

@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 資料,只需要逆向處理一下。

框架做的事情說起來是簡單的,不同開源庫實現的細節雖然不同,但是它們的核心思路很相似。

二、型別編碼 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 列舉的程式碼就不解釋了,基本上根據官方文件來的。

三、將底層資料裝進中間類

在 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訊號量保證執行緒安全。

四、一些工具方法

在進入核心業務之前,先介紹一些 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。

五、輔助類 _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 的實現的話)。

六、輔助類 _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;
                //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 協議方法是用於自定義對映關係,比如需要將 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)。

效能層面,可以在程式碼中看到兩個閃光點:

1、判斷是否是路徑

將對映的 value 拆分成 keyPath 陣列,然後做了一個遍歷,當遍歷到 @"" 空字元值時,深拷貝一份 keyPath 移除所有的 @"" 然後 break

這個操作看似簡單,實則是作者對效能的優化。通常情況下,傳入的路徑是正確的 a.b.c,這時不需要移除 @"" 。而當路徑錯誤,比如 a..b.ca.b.c. 時,分離字串時 keyPath 中就會有空值 @""。由於 componentsSeparatedByString 方法返回的是一個不可變的陣列,所以移除 keyPath 中的 @"" 需要先深拷貝一份可變記憶體。

作者此處的想法很明顯:在正常情況下,不需要移除,也就是不需要深拷貝 keyPath 增加記憶體開銷。

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

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

2、連線相同對映的屬性

之前解析 _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 的快取邏輯一樣,不多闡述。

七、給資料模型屬性賦值 / 將資料模型解析成 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。

八、從入口函式說起

回到開頭,有幾個方法是經常使用的(當然包括 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 結構體將以下內容裝起來:
1、具體模型物件(self)  
2、通過模型物件的類 Class 轉換的 _YYModelMeta 物件(modelMeta)
3、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 的實現不復雜,不多解析。

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

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

拷貝
- (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 模型轉換庫評測

相關文章