揭祕 YYModel 的魔法(上)

前言

iOS 開發中少不了各類各樣的模型,不管是採用 MVC、MVP 仍是 MVVM 設計模式都逃不過 Model。html

那麼你們在使用 Model 的時候確定遇到過一個問題,即接口傳遞過來的數據(通常是 JSON 格式)須要轉換爲 iOS 內咱們能直接使用的模型(類)。iOS 開發早期第三方框架沒有那麼多,你們可能會手寫相關代碼,可是隨着業務的擴展,模型的增多,這些沒什麼技術含量的代碼只是在重複的浪費咱們的勞動力而已。ios

這時候就須要一種工具來幫助咱們把勞動力從這些無心義的繁瑣代碼中解放出來,因而 GitHub 上出現了不少解決此類問題的第三方庫,諸如 Mantle、JSONModel、MJExtension 以及 YYModel 等等。git

這些庫的神奇之處在於它們提供了模型與 JSON 數據的自動轉換功能,彷彿具備魔法通常!本文將經過剖析 YYModel 源碼一步一步破解這「神奇」的魔法。github

YYModel 是一個高性能 iOS/OSX 模型轉換框架(該項目是 YYKit 組件之一)。YYKit 在我以前的文章【從 YYCache 源碼 Get 到如何設計一個優秀的緩存】中已經很詳細的介紹過了,感興趣的同窗能夠點進去了解一下。objective-c

YYModel 是一個很是輕量級的 JSON 模型自動轉換庫,代碼風格良好且思路清晰,能夠從源碼中看到做者對 Runtime 深厚的理解。難能難得的是 YYModel 在其輕量級的代碼下還保留着自動類型轉換,類型安全,無侵入等特性,而且具備接近手寫解析代碼的超高性能。設計模式

處理 GithubUser 數據 10000 次耗時統計 (iPhone 6):數組

索引

  • YYModel 簡介
  • YYClassInfo 剖析
  • NSObject+YYModel 探究
  • JSON 與 Model 相互轉換
  • 總結

YYModel 簡介

擼了一遍 YYModel 的源碼,果真是很是輕量級的 JSON 模型自動轉換庫,加上 YYModel.h 一共也只有 5 個文件。緩存

拋開 YYModel.h 來看,其實只有 YYClassInfo 和 NSObject+YYModel 兩個模塊。安全

  • YYClassInfo 主要將 Runtime 層級的一些結構體封裝到 NSObject 層級以便調用。
  • NSObject+YYModel 負責提供方便調用的接口以及實現具體的模型轉換邏輯(藉助 YYClassInfo 中的封裝)。

YYClassInfo 剖析

前面說到 YYClassInfo 主要將 Runtime 層級的一些結構體封裝到 NSObject 層級以便調用,我以爲若是須要與 Runtime 層級的結構體作對比的話,沒什麼比表格來的更簡單直觀了:微信

YYClassInfo Runtime
YYClassIvarInfo objc_ivar
YYClassMethodInfo objc_method
YYClassPropertyInfo property_t
YYClassInfo objc_class

Note: 本次比較基於 Runtime 源碼 723 版本。

安~ 既然是剖析確定不會列個表格這樣子哈。

YYClassIvarInfo && objc_ivar

我把 YYClassIvarInfo 看作是做者對 Runtime 層 objc_ivar 結構體的封裝,objc_ivar 是 Runtime 中表示變量的結構體。

  • YYClassIvarInfo
@interface YYClassIvarInfo : NSObject
@property (nonatomic, assign, readonly) Ivar ivar; ///< 變量,對應 objc_ivar
@property (nonatomic, strong, readonly) NSString *name; ///< 變量名稱,對應 ivar_name
@property (nonatomic, assign, readonly) ptrdiff_t offset; ///< 變量偏移量,對應 ivar_offset
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 變量類型編碼,經過 ivar_getTypeEncoding 函數獲得
@property (nonatomic, assign, readonly) YYEncodingType type; ///< 變量類型,經過 YYEncodingGetType 方法從類型編碼中獲得

- (instancetype)initWithIvar:(Ivar)ivar;
@end
複製代碼
  • objc_ivar
struct objc_ivar {
    char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 變量名稱
    char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 變量類型
    int ivar_offset OBJC2_UNAVAILABLE; // 變量偏移量
#ifdef __LP64__ // 若是已定義 __LP64__ 則表示正在構建 64 位目標
    int space OBJC2_UNAVAILABLE; // 變量空間
#endif
}
複製代碼

Note: 平常開發中 NSString 類型的屬性咱們都會用 copy 來修飾,而 YYClassIvarInfo 中的 nametypeEncoding 屬性都用 strong 修飾。由於其內部是先經過 Runtime 方法拿到 const char * 以後經過 stringWithUTF8String 方法轉爲 NSString 的。因此即使是 NSString 這類屬性在肯定其不會在初始化以後被修改的狀況下,使用 strong 作一次單純的強引用在性能上講比 copy 要高一些。

囧~ 不知道講的這麼細會不會反而引發反感,若是對文章有什麼建議能夠聯繫我 @Lision

Note: 類型編碼,關於 YYClassIvarInfo 中的 YYEncodingType 類型屬性 type 的解析代碼篇幅很長,並且沒有搬出來的必要,能夠參考官方文檔 Type EncodingsDeclared Properties 閱讀這部分源碼。

YYClassMethodInfo && objc_method

相應的,YYClassMethodInfo 則是做者對 Runtime 中 objc_method 的封裝,objc_method 在 Runtime 是用來定義方法的結構體。

  • YYClassMethodInfo
@interface YYClassMethodInfo : NSObject
@property (nonatomic, assign, readonly) Method method; ///< 方法
@property (nonatomic, strong, readonly) NSString *name; ///< 方法名稱
@property (nonatomic, assign, readonly) SEL sel; ///< 方法選擇器
@property (nonatomic, assign, readonly) IMP imp; ///< 方法實現,指向實現方法函數的函數指針
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 方法參數和返回類型編碼
@property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< 返回值類型編碼
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *argumentTypeEncodings; ///< 參數類型編碼數組

- (instancetype)initWithMethod:(Method)method;
@end
複製代碼
  • objc_method
struct objc_method {
    SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名稱
    char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法類型
    IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法實現(函數指針)
}
複製代碼

能夠看到基本也是一一對應的關係,除了類型編碼的問題做者爲了方便使用在封裝時進行了擴展。

爲了照顧對 Runtime 尚未必定了解的讀者,我這裏簡單的解釋一下 objc_method 結構體(都是我本身的認知,歡迎討論):

  • SEL,selector 在 Runtime 中的表現形式,能夠理解爲方法選擇器
typedef struct objc_selector *SEL;
複製代碼
  • IMP,函數指針,指向具體實現邏輯的函數
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
複製代碼

關於更多 Runtime 相關的知識因爲篇幅緣由(真的寫不完)就不在這篇文章介紹了,我推薦你們去魚神的文章 Objective-C Runtime 學習(由於我最先接觸 Runtime 就是經過這篇文章,笑~)。

有趣的是,魚神的文章中對 SEL 的描述有一句「其實它就是個映射到方法的 C 字符串」,可是他在文章中沒有介紹出處。本着對本身文章質量負責的原則,對於一切沒有出處的表述都應該持有懷疑的態度,因此我下面講一下本身的對於 SEL 的理解。

擼了幾遍 Runtime 源碼,發現不管是 objc-runtime-new 仍是 objc-runtime-old 中都用 SEL 類型做爲方法結構體的 name 屬性類型,並且經過如下源碼:

OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT const char * _Nonnull sel_getName(SEL _Nonnull sel)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼

能夠看到經過一個 const char * 類型的字符串便可在 Runtime 系統中註冊並返回一個 SEL,方法的名稱則會映射到這個 SEL。

官方註釋: Registers a method with the Objective-C runtime system, maps the method name to a selector, and returns the selector value.

因此我以爲 SEL 和 char * 的的確確是有某種一一對應的映射關係,不過 SEL 的本質是不是 char * 就要打一個問號了。由於我在調試 SEL 階段發現 SEL 內還有一個當前 SEL 的指針,與 char * 不一樣的是當 char * 賦值以後當前 char * 變量指針指向字符串首字符,而 SEL 則是 ,即咱們沒法直接看到它。

因此我作了一個無聊的測試,用相同的字符串初始化一個 char * 實例與一個 SEL 實例,以後嘗試打印它們,有趣的是不論我使用 %s 仍是 %c 均可以從兩個實例中獲得相同的打印輸出,不知道魚神是否作過相同的測試(笑~)

嘛~ 通過驗證咱們能夠確定 SEL 和 char * 存在某種映射關係,能夠相互轉換。同時猜想 SEL 本質上就是 char *,若是有哪位知道 SEL 與 char * 確切關係的能夠留言討論喲。

YYClassPropertyInfo && property_t

YYClassPropertyInfo 是做者對 property_t 的封裝,property_t 在 Runtime 中是用來表示屬性的結構體。

  • YYClassPropertyInfo
@interface YYClassPropertyInfo : NSObject
@property (nonatomic, assign, readonly) objc_property_t property; ///< 屬性
@property (nonatomic, strong, readonly) NSString *name; ///< 屬性名稱
@property (nonatomic, assign, readonly) YYEncodingType type; ///< 屬性類型
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 屬性類型編碼
@property (nonatomic, strong, readonly) NSString *ivarName; ///< 變量名稱
@property (nullable, nonatomic, assign, readonly) Class cls; ///< 類型
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *protocols; ///< 屬性相關協議
@property (nonatomic, assign, readonly) SEL getter; ///< getter 方法選擇器
@property (nonatomic, assign, readonly) SEL setter; ///< setter 方法選擇器

- (instancetype)initWithProperty:(objc_property_t)property;
@end
複製代碼
  • property_t
struct property_t {
    const char *name; // 名稱
    const char *attributes; // 修飾
};
複製代碼

爲何說 YYClassPropertyInfo 是做者對 property_t 的封裝呢?

// runtime.h
typedef struct objc_property *objc_property_t;

// objc-private.h
#if __OBJC2__
typedef struct property_t *objc_property_t;
#else
typedef struct old_property *objc_property_t;
#endif

// objc-runtime-new.h
struct property_t {
    const char *name;
    const char *attributes;
};
複製代碼

這裏惟一值得注意的就是 getter 與 setter 方法了。

// 先嚐試獲取屬性的 getter 與 setter
    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;
    
// 若是沒有則按照標準規則本身造
if (!_getter) {
    _getter = NSSelectorFromString(_name);
}
if (!_setter) {
    _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]);
}
複製代碼

YYClassInfo && objc_class

最後做者用 YYClassInfo 封裝了 objc_classobjc_class 在 Runtime 中表示一個 Objective-C 類。

  • YYClassInfo
@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; ///< 類
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< 超類
@property (nullable, nonatomic, assign, readonly) Class metaCls;  ///< 元類
@property (nonatomic, readonly) BOOL isMeta; ///< 元類標識,自身是否爲元類
@property (nonatomic, strong, readonly) NSString *name; ///< 類名稱
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< 父類(超類)信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< 變量信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< 方法信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< 屬性信息

- (void)setNeedUpdate;
- (BOOL)needUpdate;

+ (nullable instancetype)classInfoWithClass:(Class)cls;
+ (nullable instancetype)classInfoWithClassName:(NSString *)className;

@end
複製代碼
  • objc_class
// objc.h
typedef struct objc_class *Class;

// runtime.h
struct objc_class {
    Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa 指針

#if !__OBJC2__
    Class _Nullable super_class OBJC2_UNAVAILABLE; // 父類(超類)指針
    const char * _Nonnull name OBJC2_UNAVAILABLE; // 類名
    long version OBJC2_UNAVAILABLE; // 版本
    long info OBJC2_UNAVAILABLE; // 信息
    long instance_size OBJC2_UNAVAILABLE; // 初始尺寸
    struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 變量列表
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表
    struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 緩存
    struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 協議列表
#endif

} OBJC2_UNAVAILABLE;
複製代碼

額... 看來想徹底避開 Runtime 的知識來說 YYModel 源碼是不現實的。這裏簡單介紹一下 Runtime 中關於 Class 的知識以便閱讀,已經熟悉這方面知識的同窗就當溫習一下好了。

  • isa 指針,用於找到所屬類,類對象的 isa 通常指向對應元類。
  • 元類,因爲 objc_class 繼承於 objc_object,即類自己同時也是一個對象,因此 Runtime 庫設計出元類用以表述類對象自身所具有的元數據。
  • cache,實際上當一個對象收到消息時並不會直接在 isa 指向的類的方法列表中遍歷查找可以響應消息的方法,由於這樣效率過低了。爲了優化方法調用的效率,加入了 cache,也就是說在收到消息時,會先去 cache 中查找,找不到纔會去像上圖所示遍歷查找,相信蘋果爲了提高緩存命中率,應該也花了一些心思(笑~)。
  • version,咱們可使用這個字段來提供類的版本信息。這對於對象的序列化很是有用,它但是讓咱們識別出不一樣類定義版本中實例變量佈局的改變。

關於 Version 的官方描述: Classes derived from the Foundation framework NSObject class can set the class-definition version number using the setVersion: class method, which is implemented using the class_setVersion function.

YYClassInfo 的初始化細節

關於 YYClassInfo 的初始化細節我以爲仍是有必要分享出來的。

+ (instancetype)classInfoWithClass:(Class)cls {
    // 判空入參
    if (!cls) return nil;
    
    // 單例緩存 classCache 與 metaCache,對應緩存類和元類
    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);
        // 這裏把 dispatch_semaphore 當作鎖來使用(當信號量只有 1 時)
        lock = dispatch_semaphore_create(1);
    });
    
    // 初始化以前,首先會根據當前 YYClassInfo 是否爲元類去對應的單例緩存中查找
    // 這裏使用了上面的 dispatch_semaphore 加鎖,保證單例緩存的線程安全 
    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);
    
    // 若是沒找到,纔會去老實初始化
    if (!info) {
        info = [[YYClassInfo alloc] initWithClass:cls];
        if (info) { // 初始化成功
            // 線程安全
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            // 根據初始化信息選擇向對應的類/元類緩存注入信息,key = cls,value = info
            CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
            dispatch_semaphore_signal(lock);
        }
    }
    
    return info;
}
複製代碼

總結一下初始化的主要步驟:

  • 建立單例緩存,類緩存和元類緩存
  • 使用 dispatch_semaphore 做爲鎖保證緩存線程安全
  • 初始化前先去緩存中查找是否已經向緩存中註冊過當前要初始化的 YYClassInfo
  • 若是查找到緩存對象,則判斷緩存對象是否須要更新並執行相關操做
  • 若是緩存中未找到緩存對象則初始化
  • 初始化成功後向緩存中註冊該 YYClassInfo 實例

其中,使用緩存能夠有效減小咱們在 JSON 模型轉換時反覆初始化 YYClassInfo 帶來的開銷,而 dispatch_semaphore 在信號量爲 1 時是能夠當作鎖來使用的,雖然它在阻塞時效率超低,可是對於代碼中的緩存阻塞這裏屬於低頻事件,使用 dispatch_semaphore 在非阻塞狀態下性能很高,這裏鎖的選擇很是合適。

關於 YYClassInfo 的更新

首先 YYClassInfo 是做者對應 objc_class 封裝出來的類,因此理應在其對應的 objc_class 實例發生變化時更新。那麼 objc_class 何時會發生變化呢?

嘛~ 好比你使用了 class_addMethod 方法爲你的模型類加入了一個方法等等。

YYClassInfo 有一個私有 BOOL 類型參數 _needUpdate 用以表示當前的 YYClassInfo 實例是否須要更新,而且提供了 - (void)setNeedUpdate; 接口方便咱們在更改了本身的模型類時調用其將 _needUpdate 設置爲 YES,當 _needUpdate 爲 YES 時後面就不用我說了,相關的代碼在上一節初始化中有哦。

if (info && info->_needUpdate) {
    [info _update];
}
複製代碼

簡單介紹一下 _update,它是 YYClassInfo 的私有方法,它的實現邏輯簡單介紹就是清空當前 YYClassInfo 實例變量,方法以及屬性,以後再從新初始化它們。因爲 _update 實現源碼並無什麼特別之處,我這裏就不貼源碼了。

嘛~ 對 YYClassInfo 的剖析到這裏就差很少了。

NSObject+YYModel 探究

若是說 YYClassInfo 主要是做者對 Runtime 層在 JSON 模型轉換中須要用到的結構體的封裝,那麼 NSObject+YYModel 在 YYModel 中擔當的責任則是利用 YYClassInfo 層級封裝好的類切實的執行 JSON 模型之間的轉換邏輯,而且提供了無侵入性的接口。

第一次閱讀 NSObject+YYModel.m 的源碼可能會有些不適應,這很正常。由於其大量使用了 Runtime 函數與 CoreFoundation 庫,加上各類類型編碼和遞歸解析,代碼量也有 1800 多行了。

我簡單把 NSObject+YYModel.m 的源碼作了一下劃分,這樣劃分以後代碼看起來同樣很簡單清晰:

  • 類型編碼解析
  • 數據結構定義
  • 遞歸模型轉換
  • 接口相關代碼

類型編碼解析

類型編碼解析代碼主要集中在 NSObject+YYModel.m 的上面部分,涉及到 YYEncodingNSType 枚舉的定義,配套 YYClassGetNSType 函數將 NS 類型轉爲 YYEncodingNSType 還有 YYEncodingTypeIsCNumber 函數判斷類型是否能夠直接轉爲 C 語言數值類型的函數。

此外還有將 id 指針轉爲對應 NSNumber 的函數 YYNSNumberCreateFromID,將 NSString 轉爲 NSDate 的 YYNSDateFromString 函數,這類函數主要是方便在模型轉換時使用。

static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string) {
    typedef NSDate* (^YYNSDateParseBlock)(NSString *string);
    // YYNSDateFromString 支持解析的最長時間字符串
    #define kParserNum 34
    // 這裏建立了一個單例時間解析代碼塊數組
    // 爲了不重複建立這些 NSDateFormatter,它的初始化開銷不小
    static YYNSDateParseBlock blocks[kParserNum + 1] = {0};
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 這裏拿 `yyyy-MM-dd` 舉例分析
        {
            /* 2014-01-20 // Google */
            NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
            formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
            formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
            formatter.dateFormat = @"yyyy-MM-dd";
            // 這裏使用 blocks[10] 是由於 `yyyy-MM-dd` 的長度就是 10
            blocks[10] = ^(NSString *string) { return [formatter 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
}
複製代碼

Note: 在 iOS 7 以前 NSDateFormatter 是非線程安全的。

除此以外還用 YYNSBlockClass 指向了 NSBlock 類,實現過程也比較巧妙。

static force_inline Class YYNSBlockClass() {
    static Class cls;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        void (^block)(void) = ^{};
        cls = ((NSObject *)block).class;
        // 輪詢父類直到父類指向 NSObject 中止
        while (class_getSuperclass(cls) != [NSObject class]) {
            cls = class_getSuperclass(cls);
        }
    });
    return cls; // 拿到的就是 "NSBlock"
}
複製代碼

關於 force_inline 這種代碼技巧,我說過我在寫完 YYModel 或者攢到足夠多的時候會主動拿出來與你們分享這些代碼技巧,不過這裏你們經過字面也不難理解,就是強制內聯。

嘛~ 關於內聯函數應該不須要我多說(笑)。

數據結構定義

NSObject+YYModel 中從新定義了兩個類,經過它們來使用 YYClassInfo 中的封裝。

NSObject+YYModel YYClassInfo
_YYModelPropertyMeta YYClassPropertyInfo
_YYModelMeta YYClassInfo

_YYModelPropertyMeta

_YYModelPropertyMeta 表示模型對象中的屬性信息,它包含 YYClassPropertyInfo。

@interface _YYModelPropertyMeta : NSObject {
    @package
    NSString *_name;             ///< 屬性名稱
    YYEncodingType _type;        ///< 屬性類型
    YYEncodingNSType _nsType;    ///< 屬性在 Foundation 框架中的類型
    BOOL _isCNumber;             ///< 是否爲 CNumber
    Class _cls;                  ///< 屬性類
    Class _genericCls;           ///< 屬性包含的泛型類型,沒有則爲 nil
    SEL _getter;                 ///< getter
    SEL _setter;                 ///< setter
    BOOL _isKVCCompatible;       ///< 若是可使用 KVC 則返回 YES
    BOOL _isStructAvailableForKeyedArchiver; ///< 若是可使用 archiver/unarchiver 歸/解檔則返回 YES
    BOOL _hasCustomClassFromDictionary; ///< 類/泛型自定義類型,例如須要在數組中實現不一樣類型的轉換須要用到
    
    /* property->key: _mappedToKey:key _mappedToKeyPath:nil _mappedToKeyArray:nil property->keyPath: _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil property->keys: _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath _mappedToKeyArray:keys(array) */
    NSString *_mappedToKey;      ///< 映射 key
    NSArray *_mappedToKeyPath;   ///< 映射 keyPath,若是沒有映射到 keyPath 則返回 nil
    NSArray *_mappedToKeyArray;  ///< key 或者 keyPath 的數組,若是沒有映射多個鍵的話則返回 nil
    YYClassPropertyInfo *_info;  ///< 屬性信息,詳見上文 YYClassPropertyInfo && property_t 章節
    _YYModelPropertyMeta *_next; ///< 若是有多個屬性映射到同一個 key 則指向下一個模型屬性元
}
@end
複製代碼

_YYModelMeta

_YYModelMeta 表示模型的類信息,它包含 YYClassInfo。

@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo;
    /// Key:被映射的 key 與 keyPath, Value:_YYModelPropertyMeta.
    NSDictionary *_mapper;
    /// Array<_YYModelPropertyMeta>, 當前模型的全部 _YYModelPropertyMeta 數組
    NSArray *_allPropertyMetas;
    /// Array<_YYModelPropertyMeta>, 被映射到 keyPath 的 _YYModelPropertyMeta 數組
    NSArray *_keyPathPropertyMetas;
    /// Array<_YYModelPropertyMeta>, 被映射到多個 key 的 _YYModelPropertyMeta 數組
    NSArray *_multiKeysPropertyMetas;
    /// 映射 key 與 keyPath 的數量,等同於 _mapper.count
    NSUInteger _keyMappedCount;
    /// 模型 class 類型
    YYEncodingNSType _nsType;
    
    // 忽略
    ...
}
@end
複製代碼

遞歸模型轉換

NSObject+YYModel.m 內寫了一些(間接)遞歸模型轉換相關的函數,如 ModelToJSONObjectRecursive 之類的,因爲涉及繁雜的模型編碼解析以及代碼量比較大等緣由我不許備放在這裏詳細講解。

我認爲這種邏輯並不複雜可是牽扯較多的函數代碼與結構/類型定義代碼不一樣,後者更適合列出源碼讓讀者對數據有全面清醒的認識,而前者結合功能實例講更容易使讀者對整條功能的流程有一個更透徹的理解。

因此我準備放到後面 JSON 與 Model 相互轉換時一塊兒講。

接口相關代碼

嘛~ 理由同上。

半章總結

  • 文章對 YYModel 源碼進行了系統解讀,有條理的介紹了 YYModel 的結構,相信會讓各位對 YYModel 的代碼結構有一個清晰的認識。
  • 深刻剖析了 YYClassInfo 的 4 個類,並詳細講解了它們與 Runtime 層級結構體的對應。
  • 在剖析 YYClassInfo 章節中分享了一些我在閱讀源碼的過程當中發現的而且以爲值得分享的處理細節,好比爲何做者選擇用 strong 來修飾 NSString 等。順便還對 SEL 與 char * 的關係作了實驗得出了個人推論。
  • 把 YYClassInfo 的初始化以及更新細節單獨拎出來作了分析。
  • 探究 NSObject+YYModel 源碼(分享了一些實現細節)並對其實現代碼作了劃分,但願可以對讀者閱讀 YYModel 源碼時提供一些小小的幫助。

嘛~ 上篇差很少就這樣了。我寫的上一篇 YYKit 源碼系列文章【從 YYCache 源碼 Get 到如何設計一個優秀的緩存】收到了很多的好評和支持(掘金裏一位讀者 @ios123456 的評論更是暖化了我),這些美好的東西讓我更加堅決了繼續用心創做文章的決心。

其實我是很但願能與各位讀者多多交流的,可是又很怕遇到噴子(因此個人 我的博客 一直沒有開評論),由於我以前用心寫的文章因爲拼寫錯誤被噴了(不是指出問題那種..是真的噴),這對個人打擊很是大,因此我暫時不打算開通評論功能。

若是對文章有哪些意見能夠直接在個人微博 @Lision 聯繫我(由於社區發文以後的通知太多了,因此我把這些 push 給關了只留了微博,微博冷清嘿嘿)。


補充~ 我建了一個技術交流微信羣,想在裏面認識更多的朋友!若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~

Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。

相關文章
相關標籤/搜索