揭祕 YYModel 的魔法(上)

Lision發表於2017-11-13

前言

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

那麼大家在使用 Model 的時候肯定遇到過一個問題,即介面傳遞過來的資料(一般是 JSON 格式)需要轉換為 iOS 內我們能直接使用的模型(類)。iOS 開發早期第三方框架沒有那麼多,大家可能會手寫相關程式碼,但是隨著業務的擴充套件,模型的增多,這些沒什麼技術含量的程式碼只是在重複的浪費我們的勞動力而已。

這時候就需要一種工具來幫助我們把勞動力從這些無意義的繁瑣程式碼中解放出來,於是 GitHub 上出現了很多解決此類問題的第三方庫,諸如 Mantle、JSONModel、MJExtension 以及 YYModel 等等。

這些庫的神奇之處在於它們提供了模型與 JSON 資料的自動轉換功能,彷彿具有魔法一般!本文將通過剖析 YYModel 原始碼一步一步破解這“神奇”的魔法。

YYModel 是一個高效能 iOS/OSX 模型轉換框架(該專案是 YYKit 元件之一)。YYKit 在我之前的文章【從 YYCache 原始碼 Get 到如何設計一個優秀的快取】中已經很詳細的介紹過了,感興趣的同學可以點進去了解一下。

YYModel 是一個非常輕量級的 JSON 模型自動轉換庫,程式碼風格良好且思路清晰,可以從原始碼中看到作者對 Runtime 深厚的理解。難能可貴的是 YYModel 在其輕量級的程式碼下還保留著自動型別轉換,型別安全,無侵入等特性,並且具有接近手寫解析程式碼的超高效能。

處理 GithubUser 資料 10000 次耗時統計 (iPhone 6):

揭祕 YYModel 的魔法(上)

索引

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

YYModel 簡介

揭祕 YYModel 的魔法(上)

擼了一遍 YYModel 的原始碼,果然是非常輕量級的 JSON 模型自動轉換庫,加上 YYModel.h 一共也只有 5 個檔案。

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

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

YYClassInfo 剖析

揭祕 YYModel 的魔法(上)

前面說到 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 的知識以便閱讀,已經熟悉這方面知識的同學就當溫習一下好了。

揭祕 YYModel 的魔法(上)

  • 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 探究

揭祕 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 給關了只留了微博,微博冷清嘿嘿)。


補充~ 我建了一個技術交流微信群,想在裡面認識更多的朋友!如果各位同學對文章有什麼疑問或者工作之中遇到一些小問題都可以在群裡找到我或者其他群友交流討論,期待你的加入喲~

揭祕 YYModel 的魔法(上)

Emmmmm..由於微信群人數過百導致不可以掃碼入群,所以請掃描上面的二維碼關注公眾號進群。

相關文章