iOS源码阅读 —— YYModel

3,331 阅读7分钟

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、缓存机制等方式,减少不必要的开销。