iOS源码阅读 —— YYModel

YYModel作为一个 iOS/OSX 模型转换框架,为JSON与数据模型之间的转换,提供了高性能的解决方案。

在我个人的日常开发中,主要使用的方法有以下几个:

// JSON|字典 转 模型
+ (nullable instancetype)yy_modelWithJSON:(id)json;
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;

// 通过 JSON|字典 为 模型赋值
- (BOOL)yy_modelSetWithJSON:(id)json;
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic;

// 模型转JSON
- (NSString *)yy_modelToJSONString;

// JSON数组转模型数组
+ (nullable NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json;

由于多个功能,最终调用的方法是相同的,所以这里仅列出主要方法的代码解析。

功能

JSON转模型

+ yy_modelWithDictionary:

由于调用+ yy_modelWithJSON:方法时,方法内部先将JSON序列化为可用的字典,然后调用+ yy_modelWithDictionary:方法。所以我们直接进入+ yy_modelWithDictionary:进行分析。

代码:

/**
 通过一组 键-值对(NSDictionary),创建和返回一个新的实例
 此方法是线程安全的。

 @参数: dictionary  一组能够映射实例属性的 键-值对(dictionary)
 无效的键值对将会被忽略。
 
 @返回: 一个通过 键-值对(dictionary) 创建的新实例,出错的情况下返回nil。

 @说明: 字典中的 key 和 value 将分别映射在模型的属性名,和属性值上。
 如果值得类型不发与属性相匹配,此方法将尝试根据如下规则,进行转化:
 
     `NSString` or `NSNumber` -> c number, such as BOOL, int, long, float, NSUInteger...
     `NSString` -> NSDate, parsed with format "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss" or "yyyy-MM-dd".
     `NSString` -> NSURL.
     `NSValue` -> struct or union, such as CGRect, CGSize, ...
     `NSString` -> SEL, Class.
 */

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    
    // 创建当前类的类对象实例
    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;
}

+ yy_modelWithDictionary:方法中,主要做了三件事:1.确定类型;2.创建实例;3.为实例赋值。

1. 确定类型

在类方法中使用[self class]可以轻松获取当前类的类对象,在这里作者通过类对象创建了该类的类元_YYModelMeta *model,类元中包含了丰富的关于该类的信息。

_YYModelMeta 类元的定义:

/// 模型对象的类元信息
@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo;
    /// Key:mapped key and key path, Value:_YYModelPropertyMeta.  数据结构:{"pic": [_YYModelPropertyMeta new]}
    NSDictionary *_mapper;
    /// Array<_YYModelPropertyMeta>, 所有有效属性元的数组
    NSArray *_allPropertyMetas;
    /// Array<_YYModelPropertyMeta>, 映射到键值路径的属性元
    NSArray *_keyPathPropertyMetas;
    /// Array<_YYModelPropertyMeta>, 映射到多个键的属性元
    NSArray *_multiKeysPropertyMetas;
    /// 有效的键值对数量,所谓有效即包含 _getter、_setter、成员变量。 值与 _mapper.count 相同
    NSUInteger _keyMappedCount;
    /// 数据类型
    YYEncodingNSType _nsType;
    
    BOOL _hasCustomWillTransformFromDictionary;
    BOOL _hasCustomTransformFromDictionary;
    BOOL _hasCustomTransformToDictionary;
    BOOL _hasCustomClassFromDictionary;
}
@end

在确定类型之前,需要先判断使用者是否根据不同情况自定义了返回类的(子类)类型,即是否实现了+ modelCustomClassForDictionary:(NSDictionary *)dictionary;方法返回自定义类型。

官方示例:

@class YYCircle, YYRectangle, YYLine;

@implementation YYShape

+ (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary {
    if (dictionary[@"radius"] != nil) {
        return [YYCircle class];
    } else if (dictionary[@"width"] != nil) {
        return [YYRectangle class];
    } else if (dictionary[@"y2"] != nil) {
        return [YYLine class];
    } else {
        return [self class];
    }
}
@end

2. 创建实例

确定数据类型后,通过类对象快速创建实例。

NSObject *one = [cls new];

3. 为实例赋值

调用 -yy_modelSetWithDictionary: 方法为实例赋值。

代码:

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    
    // 创建和获取 模型的元类(包含类的详细信息)
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    
    // 判断当前类的有效属性数量
    if (modelMeta->_keyMappedCount == 0) return NO;
    
    // 判断使用者是否自定义了转换映射
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }
    
    // 创建 模型设置上下文
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic); //dic or json
    
    //  比较 元模型的键值数量 & 传入字典的键值数量
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        /**
         @function CFDictionaryApplyFunction
         对字典中的每个键值对调用函数一次。

         @param  theDict
         要查的字典。

         @param  applier
         要对字典中的每个值调用一次的回调函数。

         @param context
         一个指针大小的用户定义值,作为第三个参数传递给applier函数,但此函数不使用它。
         
         */
        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 {
        /**
         @function CFArrayApplyFunction
         对数组中的每个元素调用函数一次。

         @param theArray
         要操作的数组。

         @param range
         要将函数应用于的数组中的值范围。

         @param applier
         对数组中给定范围内的每个值调用一次的回调函数。如果此参数不是指向正确原型的函数的指针,则行为未定义。如果在应用程序函数期望的范围内存在或不能正确应用的值,则该行为是未定义的。

         @param context
         一个指针大小的用户定义值,它作为第二个参数传递给applier函数,但此函数不使用它。如果上下文不是applier函数所期望的内容,则行为是未定义的。
         
         */
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    if (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}

这里会先判断使用者是否对数据字典做了额外的处理,即是否实现了 -modelCustomWillTransformFromDictionary: 方法。如果有,则返回和使用自定义的字典。

一切准备就绪,创建模型设置上下文ModelSetContext context,准备赋值。

typedef struct {
    void *modelMeta;  ///< _YYModelMeta 类元
    void *model;      ///< id (self) 实例本身
    void *dictionary; ///< NSDictionary (json) 数据字典(json)
} ModelSetContext;

比较 类元的有效键值数量传入字典的键值数量,以较小的代价进行属性的遍历赋值(减少不必要的循环次数)。这里分别使用CFDictionaryApplyFunction()CFArrayApplyFunction() 对应 ModelSetWithDictionaryFunction()ModelSetWithPropertyMetaArrayFunction(),进行遍历调用。二者最终都是通过 ModelSetValueForProperty() 函数进行赋值的。

static void ModelSetValueForProperty(__unsafe_unretained id model,// 实例对象
                                     __unsafe_unretained id value,// 值
                                     __unsafe_unretained _YYModelPropertyMeta *meta //属性元
                                     ) 

ModelSetValueForProperty() 函数中对属性的数据进行了详细的类型判断,主要分为三大类(C的基础数据类型、Foundation的NS数据类型、自定义数据类型)。除了C的基本数据类型,后者都通过消息发送 objc_msgSend 的方式,调用属性的 meta->_setter 方法进行赋值。

由于实现代码较长,这里就不展示了,有兴趣的可以自行查看源码:《YYModel/NSObject+YYModel.m》第784~1098行

到此,JSON转模型的工作就完成了。

模型转JSON

+ yy_modelToJSONString:

- (id)yy_modelToJSONObject {
    /*
     Apple said:
     The top level object is an NSArray or NSDictionary.
     All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
     All dictionary keys are instances of NSString.
     Numbers are not NaN or infinity.
     */
    id jsonObject = ModelToJSONObjectRecursive(self);
    if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
    if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;
    return nil;
}

- (NSData *)yy_modelToJSONData {
    id jsonObject = [self yy_modelToJSONObject];
    if (!jsonObject) return nil;
    return [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:NULL];
}

- (NSString *)yy_modelToJSONString {
    NSData *jsonData = [self yy_modelToJSONData];
    if (jsonData.length == 0) return nil;
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

从方法实现中不难看出,模型转JSON主要依赖于递归函数 ModelToJSONObjectRecursive,该函数最终将返回一个有效的JSON对象(NSArray/NSDictionary/NSString/NSNumber/NSNull)。

ModelToJSONObjectRecursive 内部实现代码拆解:

if (!model || model == (id)kCFNull) return model;
if ([model isKindOfClass:[NSString class]]) return model;
if ([model isKindOfClass:[NSNumber class]]) return model;
if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model];
if ([model isKindOfClass:[NSData class]]) return nil;

当模型值符合或接近目标类型时,可做简单的转换或直接返回.

// 字典
if ([model isKindOfClass:[NSDictionary class]]) {
    if ([NSJSONSerialization isValidJSONObject:model]) return model;
    NSMutableDictionary *newDic = [NSMutableDictionary new];
    [((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
        NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
        if (!stringKey) return;
        id jsonObj = ModelToJSONObjectRecursive(obj);
        if (!jsonObj) jsonObj = (id)kCFNull;
        newDic[stringKey] = jsonObj;
    }];
    return newDic;
}

// 集合
if ([model isKindOfClass:[NSSet class]]) {
    NSArray *array = ((NSSet *)model).allObjects;
    if ([NSJSONSerialization isValidJSONObject:array]) return array;
    NSMutableArray *newArray = [NSMutableArray new];
    for (id obj in array) {
        if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
            [newArray addObject:obj];
        } else {
            id jsonObj = ModelToJSONObjectRecursive(obj);
            if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
        }
    }
    return newArray;
}

// 数组
if ([model isKindOfClass:[NSArray class]]) {
    if ([NSJSONSerialization isValidJSONObject:model]) return model;
    NSMutableArray *newArray = [NSMutableArray new];
    for (id obj in (NSArray *)model) {
        if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
            [newArray addObject:obj];
        } else {
            id jsonObj = ModelToJSONObjectRecursive(obj);
            if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
        }
    }
    return newArray;
}

当模型值为字典、集合数组类型时,需要遍历和递归其内部元素,直至逐一转化成有效的JSON对象。

// 自定义类
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]];
if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil;
NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64];
__unsafe_unretained NSMutableDictionary *dic = result; // avoid retain and release in block
[modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
    if (!propertyMeta->_getter) return;

    id value = nil;
    if (propertyMeta->_isCNumber) {
        value = ModelCreateNumberFromProperty(model, propertyMeta);
    } else if (propertyMeta->_nsType) {
        id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
        value = ModelToJSONObjectRecursive(v);
    } else {
        switch (propertyMeta->_type & YYEncodingTypeMask) {
            case YYEncodingTypeObject: {
                id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                value = ModelToJSONObjectRecursive(v);
                if (value == (id)kCFNull) value = nil;
            } break;
            case YYEncodingTypeClass: {
                Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                value = v ? NSStringFromClass(v) : nil;
            } break;
            case YYEncodingTypeSEL: {
                SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                value = v ? NSStringFromSelector(v) : nil;
            } break;
            default: break;
        }
    }
    if (!value) return;

    if (propertyMeta->_mappedToKeyPath) {
        NSMutableDictionary *superDic = dic;
        NSMutableDictionary *subDic = nil;
        for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
            NSString *key = propertyMeta->_mappedToKeyPath[i];
            if (i + 1 == max) { // end
                if (!superDic[key]) superDic[key] = value;
                break;
            }

            subDic = superDic[key];
            if (subDic) {
                if ([subDic isKindOfClass:[NSDictionary class]]) {
                    subDic = subDic.mutableCopy;
                    superDic[key] = subDic;
                } else {
                    break;
                }
            } else {
                subDic = [NSMutableDictionary new];
                superDic[key] = subDic;
            }
            superDic = subDic;
            subDic = nil;
        }
    } else {
        if (!dic[propertyMeta->_mappedToKey]) {
            dic[propertyMeta->_mappedToKey] = value;
        }
    }
}];

当模型值为自定义类型时,需要遍历和递归其映射表_mapper({属性名: 属性元}),通过消息发送 objc_msgSend 的方式,调用属性的 meta->_getter 方法进行取值,直至逐一转化成有效的JSON对象。

if (modelMeta->_hasCustomTransformToDictionary) {
    // 校验数据
    BOOL suc = [((id<YYModel>)model) modelCustomTransformToDictionary:dic];
    if (!suc) return nil;
}
return result;

最后,判断使用者是否有额外的转换处理,并并校验数据的有效性。

注意:resultdic 指向的是同一个实例,所以如果 dic 在外部函数中被修改了,等同于修改了 result

总结

  • YYModel的使用无侵入性,采用Category的方式实现功能,比较灵活。
  • 容错方面,YYModel对数据类型做了详细的分类和判断,就算转换失败,也会自动留空(nil)。
  • 性能方面,使用 CoreFoundation、内联函数、runtime、缓存机制等方式,减少不必要的开销。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352