經(jīng)典iOS第三方庫源碼分析 - YYModel

YYModel介紹

YYModel是一個針對iOS/OSX平臺的高性能的Model解析庫,是屬于YYKit的一個組件背零,創(chuàng)建是ibireme迎吵。

其實在YYModel出現(xiàn)之前,已經(jīng)有非常多的Model解析庫桑逝,例如JSONModelMantleMJExtension俏让。

YYModel從易用性和性能方面均達到了最高水平楞遏。

性能

Model解析庫對比

特性

  • High performance: The conversion performance is close to handwriting code.
  • Automatic type conversion: The object types can be automatically converted.
  • Type Safe: All data types will be verified to ensure type-safe during the conversion process.
  • Non-intrusive: There is no need to make the model class inherit from other base class.
  • Lightwight: This library contains only 5 files.
  • Docs and unit testing: 100% docs coverage, 99.6% code coverage.

YYModel使用

簡單Model和JSON轉(zhuǎn)換

// JSON:
{
    "uid":123456,
    "name":"Harry",
    "created":"1965-07-31T00:00:00+0000"
}

// Model:
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end
@implementation User
@end

// Convert json to model:
User *user = [User yy_modelWithJSON:json];
    
// Convert model to json:
NSDictionary *json = [user yy_modelToJSONObject];

內(nèi)嵌Model

// JSON
{
    "author":{
        "name":"J.K.Rowling",
        "birthday":"1965-07-31T00:00:00+0000"
    },
    "name":"Harry Potter",
    "pages":256
}

// Model: (no need to do anything)
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end
    
@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author;
@end
@implementation Book
@end

集合類型 - Array、Set

@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end

@implementation Attributes
+ (NSDictionary *)modelContainerPropertyGenericClass {
    // value should be Class or Class name.
    return @{@"shadows" : [Shadow class],
             @"borders" : Border.class,
             @"attachments" : @"Attachment" };
}
@end

YYModel代碼結(jié)構(gòu)

YYModel整個項目非常簡潔首昔,只有5個文件寡喝。

文件 描述
NSObject+YYModel YYModel對于NSObject的擴展
YYClassInfo 類信息
YYModel.h YYModel的頭文件

詳細分析

以一個例子來分析,外部是Book對象勒奇,內(nèi)部有一個Author對象预鬓。

    NSString *json = @"{ \
    \"author\":{ \
        \"name\":\"J.K.Rowling\", \
        \"birthday\":\"1965-07-31T00:00:00+0000\" \
    }, \
    \"name\":\"Harry Potter\", \
    \"pages\":256 \
    }";
    
    Book *book = [Book yy_modelWithJSON:json];

yy_modelWithJSON

入口從[NSObject yy_modelWithJSON]進入

+ (instancetype)yy_modelWithJSON:(id)json {
    NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
    return [self yy_modelWithDictionary:dic];
}

_yy_dictionaryWithJSON:將JSON的數(shù)據(jù)(String或者NSData)轉(zhuǎn)換成NSDictionary,主要使用系統(tǒng)方法[NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];

yy_modelWithDictionary

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    ...
    Class cls = [self class];
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    
    NSObject *one = [cls new];
    if ([one yy_modelSetWithDictionary:dictionary]) return one;
    return nil;
}

modelCustomClassForDictionary - Model類可以重載這個方法赊颠,將JSON轉(zhuǎn)換成另外一個Model類
后續(xù)處理都放在了yy_modelSetWithDictionary這個方法

yy_modelSetWithDictionary

首先根據(jù)Class信息構(gòu)造出_YYModelMeta

_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];

_YYModelMeta中包含如下屬性:

  • YYClassInfo *_classInfo:類信息格二,例如class、superclass竣蹦、ivarInfo顶猜、methodInfos、propertyInfos
  • NSDictionary *_mapper:屬性key和對應的_YYModelPropertyMeta
{
    author = "<_YYModelPropertyMeta: 0x6080000f5c00>";
    name = "<_YYModelPropertyMeta: 0x6080000f5b00>";
    pages = "<_YYModelPropertyMeta: 0x6080000f5b80>";
}

看下Name里面對應的_YYModelPropertyMeta的內(nèi)容:


_YYModelPropertyMeta
* _name: 對應的是property的名字
* _nsType:對應property的類型
* _getter:getter方法
* _setter:setter方法
  • NSArray *_allPropertyMetas:所有的_YYModelPropertyMeta
  • NSArray *_keyPathPropertyMetas:Array<_YYModelPropertyMeta>, property meta which is mapped to a key path
  • NSArray *_multiKeysPropertyMetas:Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.

數(shù)據(jù)填充

    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);
    }

CFDictionaryApplyFunction/CFArrayApplyFunction:針對NSDictionary和NSArray的每一個值草添,執(zhí)行一個方法驶兜。Context作為方法中一個參數(shù)扼仲,帶入了Model的信息远寸。

Context數(shù)據(jù)結(jié)構(gòu)如下:

typedef struct {
    void *modelMeta;  ///< _YYModelMeta
    void *model;      ///< id (self)
    void *dictionary; ///< NSDictionary (json)
} ModelSetContext;

ModelSetWithDictionaryFunction

static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
    __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
    __unsafe_unretained id model = (__bridge id)(context->model);
    while (propertyMeta) {
        if (propertyMeta->_setter) {
            ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
        }
        propertyMeta = propertyMeta->_next;
    };
}

這個方法是將Dictionary的數(shù)據(jù)填充到Model的核心過程。
通過Context獲取meta(Model的類信息)屠凶,通過meta獲取當前Key的propertyMeta(屬性信息)驰后,遞歸調(diào)用ModealSetValueForProperty填充model里面對應Key的Property。

ModelSetValueForProperty

這個方法會將數(shù)據(jù)填充到Model對應的Property中矗愧。

對于普通數(shù)據(jù)類型的數(shù)據(jù)填充灶芝,大體如下:

switch (meta->_nsType) {
                case YYEncodingTypeNSString:
                case YYEncodingTypeNSMutableString: {
                    if ([value isKindOfClass:[NSString class]]) {
                        if (meta->_nsType == YYEncodingTypeNSString) {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
                        } else {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
                        }
                    }

對于內(nèi)嵌的對象屬性郑原,處理如下:
Value通常是一個NSDicationary,如果有g(shù)etter方法夜涕,獲取這個property的對象犯犁,如果為nill則創(chuàng)建一個實例,再通過[one yy_modelSetWithDictionary:value]女器,填充這個property對象酸役。

            case YYEncodingTypeObject: {
                ...
                else if ([value isKindOfClass:[NSDictionary class]]) {
                    NSObject *one = nil;
                    if (meta->_getter) {
                        one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);
                    }
                    if (one) {
                        [one yy_modelSetWithDictionary:value];
                    } else {
                        Class cls = meta->_cls;
                        if (meta->_hasCustomClassFromDictionary) {
                            cls = [cls modelCustomClassForDictionary:value];
                            if (!cls) cls = meta->_genericCls; // for xcode code coverage
                        }
                        one = [cls new];
                        [one yy_modelSetWithDictionary:value];
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one);
                    }
                }
            } break;

最佳實踐

force_inline

在YYModel實現(xiàn)中大量使用force_inline關(guān)鍵詞來修飾方法,inline的作用可以參考Wikipedia: Inline Function驾胆。Inline Function會在編譯階段將方法實現(xiàn)直接拷貝到調(diào)用處涣澡,減少方法參數(shù)傳遞和查找,可以提高運行效率丧诺。

YYMode的使用方法如下:

#define force_inline __inline__ __attribute__((always_inline))

static force_inline YYEncodingNSType YYClassGetNSType(Class cls) {
    ...
}

一次性初始化

對于一次性初始化的代碼盡量放在dispatch_once block中入桂,保證只會初始化一次。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    lock = dispatch_semaphore_create(1);
});

Lock

通過Lock來保證多線程執(zhí)行的一致性

static dispatch_semaphore_t lock;
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(lock);

緩存的實現(xiàn)

通過CFDictionaryCreateMutable實現(xiàn)了一個簡易的文件緩存驳阎,注意在讀取和寫入緩存的時候都使用了Lock來保證多線程一致性抗愁。

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

總結(jié)

YYModel是一個非常簡潔、高性能的Model解析庫呵晚,作者使用了大量的runtime方式解析class內(nèi)部信息驹愚,使用了inline、緩存劣纲、Lock等方式提高了性能和安全性逢捺。
多讀經(jīng)典的開源庫,理解作者的實現(xiàn)方式癞季,對于提高iOS設計和編程能力有很大的幫助劫瞳。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市绷柒,隨后出現(xiàn)的幾起案子志于,更是在濱河造成了極大的恐慌,老刑警劉巖废睦,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伺绽,死亡現(xiàn)場離奇詭異,居然都是意外死亡嗜湃,警方通過查閱死者的電腦和手機奈应,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來购披,“玉大人杖挣,你說我怎么就攤上這事「斩福” “怎么了惩妇?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵株汉,是天一觀的道長。 經(jīng)常有香客問我歌殃,道長乔妈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任氓皱,我火速辦了婚禮褒翰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匀泊。我一直安慰自己优训,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布各聘。 她就那樣靜靜地躺著揣非,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躲因。 梳的紋絲不亂的頭發(fā)上早敬,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音大脉,去河邊找鬼搞监。 笑死,一個胖子當著我的面吹牛镰矿,可吹牛的內(nèi)容都是我干的琐驴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼秤标,長吁一口氣:“原來是場噩夢啊……” “哼绝淡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起苍姜,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤牢酵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后衙猪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體馍乙,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年垫释,在試婚紗的時候發(fā)現(xiàn)自己被綠了丝格。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡饶号,死狀恐怖铁追,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茫船,我是刑警寧澤琅束,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站算谈,受9級特大地震影響涩禀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜然眼,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一艾船、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧高每,春花似錦屿岂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至带欢,卻和暖如春运授,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乔煞。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工吁朦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人渡贾。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓逗宜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親空骚。 傳聞我的和親對象是個殘疾皇子锦溪,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容