MJExtension源码分析

  • 实现我们的模型是这样的,只有2个属性

iOS开发中经常会用到数据和模型的互相转换,大致有两种转换方式:1.手动写转换的代码,2.利用开源库进行转换。常用的开源库有:JSONModelMantleMJExtensionYYModel等等,本文主要介绍一下MJExtension的底层实现,看一看小码哥如何设计这个轻量级的数模转换框架。

iOS开发中经常会用到数据和模型的互相转换,大致有两种转换方式:1.手动写转换的代码,2.利用开源库进行转换。常用的开源库有:JSONModelMantleMJExtensionYYModel等等,本文主要介绍一下MJExtension的底层实现,看一看小码哥如何设计这个轻量级的数模转换框架。

iOS开发中经常会用到数据和模型的互相转换,大致有两种转换方式:1.手动写转换的代码,2.利用开源库进行转换。常用的开源库有:JSONModelMantleMJExtensionYYModel等等,本文主要介绍一下MJExtension的底层实现,看一看小码哥如何设计这个轻量级的数模转换框架。

写在前面

上一篇:runtime:一句代码实现对象NSCoding主要实现了运行时遍历对象属性来归档的功能,类似于NSObject+MJCoding的中的实现,但是对于MJExtension框架中的分析还不够透彻,这篇我尝试对MJExtension中的关于NSCoding的部分进行解读分析。主要代码来自MJExtension中的NSObject+MJClassNSObject+MJCoding

本着面向应用的角度,我觉得还是从一个字典转模型的例子入手,来详细介绍一下MJExtension的转换过程。

本着面向应用的角度,我觉得还是从一个字典转模型的例子入手,来详细介绍一下MJExtension的转换过程。

本着面向应用的角度,我觉得还是从一个字典转模型的例子入手,来详细介绍一下MJExtension的转换过程。

NSObject+MJClass

NSObject+MJClass中主要有以下方法:

图片 1

NSObject+MJClass.png

  • 遍历所有的类
    mj_enumerateClasses
    mj_enumerateAllClasses

  • 关于黑白名单的配置方法有两种:
    通过block来return一个数组。
    实体类中重写对应的方法,方法中返回一个数组
    特别注意:两种的写法不同,但是作用是一样的!例如下这两个:

//通过block返回数组
typedef NSArray * (^MJAllowedPropertyNames)();
+ (void)mj_setupAllowedPropertyNames:(MJAllowedPropertyNames)allowedPropertyNames;

//通过方法返回一个数组
+ (NSMutableArray *)mj_totalAllowedPropertyNames;
@interface BMUser : NSObject@property (nonatomic, copy) NSString *name; ///< name@property (nonatomic, assign) int age; ///< age@end

待转换的字典对象:

    NSDictionary *dict = @{
        @"name": @"kelvin",
        @"age": @18,
        @"married": @"false",
        @"computer": {
            @"type": @"AUSU",
            @"date": @"2012-01"
        },
        @"skills": @[
            @{
                @"type": @"C language"
                @"degree": @"proficient"
            },
            @{
                @"type": @"Python language",
                @"degree": @"grasp"
            }
        ],
    };

待转换的字典对象:

    NSDictionary *dict = @{
        @"name": @"kelvin",
        @"age": @18,
        @"married": @"false",
        @"computer": {
            @"type": @"AUSU",
            @"date": @"2012-01"
        },
        @"skills": @[
            @{
                @"type": @"C language"
                @"degree": @"proficient"
            },
            @{
                @"type": @"Python language",
                @"degree": @"grasp"
            }
        ],
    };

待转换的字典对象:

    NSDictionary *dict = @{
        @"name": @"kelvin",
        @"age": @18,
        @"married": @"false",
        @"computer": {
            @"type": @"AUSU",
            @"date": @"2012-01"
        },
        @"skills": @[
            @{
                @"type": @"C language"
                @"degree": @"proficient"
            },
            @{
                @"type": @"Python language",
                @"degree": @"grasp"
            }
        ],
    };

mj_enumerateClasses和mj_enumerateAllClasses两个方法的区别

通过查看代码,发现mj_enumerateClasses中多了这么一行

if ([MJFoundation isClassFromFoundation:c]) break;

查看位于MJFoundation中的isClassFromFoundation这个方法的具体实现:

+ (BOOL)isClassFromFoundation:(Class)c
{
    if (c == [NSObject class] || c == [NSManagedObject class]) return YES;

    __block BOOL result = NO;
    [[self foundationClasses] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) {
        if ([c isSubclassOfClass:foundationClass]) {
            result = YES;
            *stop = YES;
        }
    }];
    return result;
}

+ (NSSet *)foundationClasses
{
    if (foundationClasses_ == nil) {
        // 集合中没有NSObject,因为几乎所有的类都是继承自NSObject,具体是不是NSObject需要特殊判断
        foundationClasses_ = [NSSet setWithObjects:
                              [NSURL class],
                              [NSDate class],
                              [NSValue class],
                              [NSData class],
                              [NSError class],
                              [NSArray class],
                              [NSDictionary class],
                              [NSString class],
                              [NSAttributedString class], nil];
    }
    return foundationClasses_;
}

通过以上两个方法,相信可以很明确的确定mj_enumerateClasses把NSObjectNSManagedObject,以及foundationClasses集合中的诸如NSURL等排除在外,只能遍历得到自定义的Class。而mj_enumerateAllClasses则是可以得到包含了系统的Class,例如NSObject。从名字我们也可以明显看出来。

  • 使用最原始的方法把json->Models时,代码如下

转换流程

转换流程

转换流程

配置黑白名单,有两种方式:

  • 通过block来return一个数组;
  • 重写配置方法,return一个数组

1.实现转换的代码

MJExtension的转换十分方便,下面一行代码就可以搞定,当然在转换之前,要设置字典中数组对应的对象模型,因为在转换过程中,需要根据模型类来创建模型数组。

    // Person.m
    + (NSDictionary*)mj_objectClassInArray
    {
        return @{@"skills": @"Skill"};
    }

    // 使用过程的转换代码
    Person *p = [Person mj_objectWithKeyValues:dict];

1.实现转换的代码

MJExtension的转换十分方便,下面一行代码就可以搞定,当然在转换之前,要设置字典中数组对应的对象模型,因为在转换过程中,需要根据模型类来创建模型数组。

    // Person.m
    + (NSDictionary*)mj_objectClassInArray
    {
        return @{@"skills": @"Skill"};
    }

    // 使用过程的转换代码
    Person *p = [Person mj_objectWithKeyValues:dict];

1.实现转换的代码

MJExtension的转换十分方便,下面一行代码就可以搞定,当然在转换之前,要设置字典中数组对应的对象模型,因为在转换过程中,需要根据模型类来创建模型数组。

    // Person.m
    + (NSDictionary*)mj_objectClassInArray
    {
        return @{@"skills": @"Skill"};
    }

    // 使用过程的转换代码
    Person *p = [Person mj_objectWithKeyValues:dict];

通过block传入,白名单和黑名单的数据设置

//属性白名单配置(用于模型字典转化)
+ (void)mj_setupAllowedPropertyNames:(MJAllowedPropertyNames)allowedPropertyNames;

//属性黑名单配置
+ (void)mj_setupIgnoredPropertyNames:(MJIgnoredPropertyNames)ignoredPropertyNames;

//属性归档白名单配置(用于归档)
+ (void)mj_setupAllowedCodingPropertyNames:(MJAllowedCodingPropertyNames)allowedCodingPropertyNames;

//属性归档黑名单配置
+ (void)mj_setupIgnoredCodingPropertyNames:(MJIgnoredCodingPropertyNames)ignoredCodingPropertyNames;

从代码可以发现,以上四个方法,调用了同一个方法。而且注释了“内部使用”

#pragma mark - 内部使用
+ (void)mj_setupBlockReturnValue:(id (^)())block key:(const char *)key;

原来是因为,业务逻辑是一致的,使用过了key来作为四种情况的区分,内部已经定义好了key,分别对应四种情况:

static const char MJAllowedPropertyNamesKey = ‘\0’;
static const char MJIgnoredPropertyNamesKey = ‘\0’;
static const char MJAllowedCodingPropertyNamesKey = ‘\0’;
static const char MJIgnoredCodingPropertyNamesKey = ‘\0’;

内部使用的方法mj_setupBlockReturnValue的实现是这样的:

+ (void)mj_setupBlockReturnValue:(id (^)())block key:(const char *)key
{
    if (block) {
        objc_setAssociatedObject(self, key, block(), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    } else {
        objc_setAssociatedObject(self, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    // 清空数据
    [[self dictForKey:key] removeAllObjects];
}

objc_setAssociatedObject是不是很眼熟!没错,这个是用来做属性关联的,又是key又是objc_setAssociatedObject的!
原来就是把从外部接收的block中的数组,动态关联到当前的对象。关联完毕,再清空当前的数组removeAllObjects,以便于下次的循环获取到的对象重新赋值,达到不重复关联数据的目的。

// 1.获取json数据NSString *json = @"{\"name\" : \"idhong\", \"age\" : 15}";// 2.json->NSDictionaryNSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:NULL];// 3.创建userBMUser *user = [BMUser new];// 4.赋值user.name = jsonDict[@"name"];user.age = [jsonDict[@"age"] intValue];// 其他属性设置NSLog;

2.具体的转换过程

在不考虑CoreData的情况下,我们直接来看转换的核心代码的方法,主要就是mj_setKeyValues:context:方法来完成字典转模型的工作

/**
 核心代码:
 */
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context

把传进来的数据转换为字典

可以看到,传进来的数据还可以是NSString
NSData类型,最后都被转换为JSON格式的数据

    // NSObject+MJKeyValue.m
    // 获得JSON对象
    keyValues = [keyValues mj_JSONObject];

--->

    if ([self isKindOfClass:[NSString class]]) {
        return [NSJSONSerialization JSONObjectWithData:[((NSString *)self) dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    } else if ([self isKindOfClass:[NSData class]]) {
        return [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
    }

    // 返回字典
    return self.mj_keyValues;

获取转换的白名单和黑名单

首先检查名单是不是存在,如果存在,就直接返回;不存在,则根据方法mj_totalAllowedPropertyNames来获取名单,然后返回

    // NSObject+MJKeyValue.m
    Class clazz = [self class];
    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

--->
    // NSObject+MJClass.m
    + (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key
    {
        NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
        if (array) return array;

        // 创建、存储
        [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];

        if ([self respondsToSelector:selector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            NSArray *subArray = [self performSelector:selector];
    #pragma clang diagnostic pop
            if (subArray) {
                [array addObjectsFromArray:subArray];
            }
        }

        [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
            NSArray *subArray = objc_getAssociatedObject(c, key);
            [array addObjectsFromArray:subArray];
        }];
        return array;
    }

根据上述的名单,来判断属性是否需要被忽略

    // 检测是否被忽略
    if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
    if ([ignoredPropertyNames containsObject:property.name]) return;

遍历成员变量

+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration遍历所有的属性。属性会先从缓存cachedProperties中取,如果存在,直接返回;如果不存在,就遍历类来获取属性列表,创建MJProperty对象,并设置它的name(属性名)、type(数据类型)、srcClass(来源类)等等;同时,还会设置propertyKeysDict(类对应的字典中的所有key)
objectClassInArrayDict(类对应的字典数组中的模型类)。

    // NSObject+MJProperty.m
    // 获得成员变量
    NSArray *cachedProperties = [self properties];

    // 遍历成员变量
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        enumeration(property, &stop);
        if (stop) break;
    }

---->

    + (NSMutableArray *)properties
    {
        NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];

        if (cachedProperties == nil) {
            cachedProperties = [NSMutableArray array];

            [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
                // 1.获得所有的成员变量
                unsigned int outCount = 0;
                objc_property_t *properties = class_copyPropertyList(c, &outCount);

                // 2.遍历每一个成员变量
                for (unsigned int i = 0; i<outCount; i++) {
                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];

                    NSLog(@"%@ - %@", property.name, property.srcClass);
                    // 过滤掉Foundation框架类里面的属性
                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                    property.srcClass = c;
                    [property setOriginKey:[self propertyKey:property.name] forClass:self];
                    [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
                    [cachedProperties addObject:property];
                }

                // 3.释放内存
                free(properties);
            }];

            [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
        }

        return cachedProperties;
    }    

在上述设置模型属性时,会调用mj_replacedKeyFromPropertyName查看是否需要替换为新的键值。通常,特殊字符如:iddescription不能作为对象属性名,所以如果确实需要该属性名时,可以用IDdesc等代替,然后就需要实现该方法做替换,使对象的ID属性名对应字典中的id键。

    [property setOriginKey:[self propertyKey:property.name] forClass:self];

---->

    // NSObject+MJProperty.m
    + (id)propertyKey:(NSString *)propertyName
    {
        ...
        // 查看有没有需要替换的key
        if ((!key || [key isEqual:propertyName]) && [self respondsToSelector:@selector(mj_replacedKeyFromPropertyName)]) {
            key = [self mj_replacedKeyFromPropertyName][propertyName];
        }
        ...
    }

取出字典中的属性值

根据变量名获取字典中对应的值

    // 取出属性值
    id value;
    NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
    for (NSArray *propertyKeys in propertyKeyses) {
        value = keyValues;
        for (MJPropertyKey *propertyKey in propertyKeys) {
            value = [propertyKey valueInObject:value];
        }
        if (value) break;
    }

过滤值

如果用户需要对取得的字典中的值进行特殊处理,则需要实现mj_newValueFromOldValue:property:方法,并返回对应key的新值。如修改日期格式等等

    // NSObject+MJKeyValue.m
    // 值的过滤
    id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
    if (newValue != value) { // 有过滤后的新值
        [property setValue:newValue forObject:self];
        return;
    }

---->

    // NSObject+MJProperty.m
    // 如果有实现方法
    if ([object respondsToSelector:@selector(mj_newValueFromOldValue:property:)]) {
        return [object mj_newValueFromOldValue:oldValue property:property];
    }
    // 兼容旧版本
    if ([self respondsToSelector:@selector(newValueFromOldValue:property:)]) {
        return [self performSelector:@selector(newValueFromOldValue:property:)  withObject:oldValue  withObject:property];
    }

    // 查看静态设置
    __block id newValue = oldValue;
    [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
        MJNewValueFromOldValue block = objc_getAssociatedObject(c, &MJNewValueFromOldValueKey);
        if (block) {
            newValue = block(object, oldValue, property);
            *stop = YES;
        }
    }];
    return newValue;    

获取变量的类型

根据property属性获取属性的类型(基本数据类型则为nil)和数组中的成员对象类型(如果属性不是数组,则为空对象类型)

    // 复杂处理
    MJPropertyType *type = property.type;
    Class propertyClass = type.typeClass;
    Class objectClass = [property objectClassInArrayForClass:[self class]];

转换为可变类型

如果对象的属性是可变类型,但是从字典中取出的值都是不可变类型,那么需要把取出的值转换为可变类型

    // 不可变 -> 可变处理
    if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
        value = [NSMutableArray arrayWithArray:value];
    } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
        value = [NSMutableDictionary dictionaryWithDictionary:value];
    } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
        value = [NSMutableString stringWithString:value];
    } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
        value = [NSMutableData dataWithData:value];
    }

模型属性

这里用到了是不是Foundation的判断,如果属性是自定义属性,就递归调用mj_objectWithKeyValues再次进行赋值

    if (!type.isFromFoundation && propertyClass) { // 模型属性
        value = [propertyClass mj_objectWithKeyValues:value context:context];
    }

数组属性

如果是NSURL的对象类型,就获取它的url字符串,然后组合成数组;如果是其它对象类型,就调用mj_objectArrayWithKeyValuesArray::转换为模型数组

    if (objectClass) {
        if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
            // string array -> url array
            NSMutableArray *urlArray = [NSMutableArray array];
            for (NSString *string in value) {
                if (![string isKindOfClass:[NSString class]]) continue;
                [urlArray addObject:string.mj_url];
            }
            value = urlArray;
        } else { // 字典数组-->模型数组
            value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
        }
    }

处理模型属性和数组属性之外的其它属性

如果属性的类型和字典中对应的值的类型不匹配,就进行转换,如:NSNumber -> NSStringNSURL -> NSStringNSString -> NSURL,
NSString -> NSURL。另外,针对属性是布尔类型,而值是字符串:yes/trueno/false(忽略大小写情况),则转换为YESNO的包装类型。最后,如果属性和值的类型依然不匹配,那么就把值设为空,给后面KVC使用

    if (propertyClass == [NSString class]) {
        if ([value isKindOfClass:[NSNumber class]]) {
            // NSNumber -> NSString
            value = [value description];
        } else if ([value isKindOfClass:[NSURL class]]) {
            // NSURL -> NSString
            value = [value absoluteString];
        }
    } else if ([value isKindOfClass:[NSString class]]) {
        if (propertyClass == [NSURL class]) {
            // NSString -> NSURL
            // 字符串转码
            value = [value mj_url];
        } else if (type.isNumberType) {
            NSString *oldValue = value;

            // NSString -> NSNumber
            if (type.typeClass == [NSDecimalNumber class]) {
                value = [NSDecimalNumber decimalNumberWithString:oldValue];
            } else {
                value = [numberFormatter_ numberFromString:oldValue];
            }

            // 如果是BOOL
            if (type.isBoolType) {
                // 字符串转BOOL(字符串没有charValue方法)
                // 系统会调用字符串的charValue转为BOOL类型
                NSString *lower = [oldValue lowercaseString];
                if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) {
                    value = @YES;
                } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
                    value = @NO;
                }
            }
        }
    }

    // value和property类型不匹配
    if (propertyClass && ![value isKindOfClass:propertyClass]) {
        value = nil;
    }

赋值

利用KVC赋值

    [property setValue:value forObject:self];

归档

MJExtensionCodingImplementation这个宏表明对象支持归档,通过分类支持NSCoding协议,实现归档和解档的方法,我们可以直接把对象归档和解档,十分方便

宏定义函数

这个宏可以构建错误日志并输出,方便调试。同时,对于宏替换不是十分了解的时候,可以使用预处理命令,把文件的宏全部替换后,就可以十分方便的看到宏被替换后的代码模样:

# clang -E 源文件 [-o 目标文件]

    // 判断参数是不是字典
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");

---->
    // MJExtensionConst.h
    #define MJExtensionAssertError(condition, returnValue, clazz, msg) \
    [clazz setMj_error:nil]; \
    if ((condition) == NO) { \
        MJExtensionBuildError(clazz, msg); \
        return returnValue;\
    }

2.具体的转换过程

在不考虑CoreData的情况下,我们直接来看转换的核心代码的方法,主要就是mj_setKeyValues:context:方法来完成字典转模型的工作

/**
 核心代码:
 */
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context

把传进来的数据转换为字典

可以看到,传进来的数据还可以是NSString
NSData类型,最后都被转换为JSON格式的数据

    // NSObject+MJKeyValue.m
    // 获得JSON对象
    keyValues = [keyValues mj_JSONObject];

--->

    if ([self isKindOfClass:[NSString class]]) {
        return [NSJSONSerialization JSONObjectWithData:[((NSString *)self) dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    } else if ([self isKindOfClass:[NSData class]]) {
        return [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
    }

    // 返回字典
    return self.mj_keyValues;

获取转换的白名单和黑名单

首先检查名单是不是存在,如果存在,就直接返回;不存在,则根据方法mj_totalAllowedPropertyNames来获取名单,然后返回

    // NSObject+MJKeyValue.m
    Class clazz = [self class];
    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

--->
    // NSObject+MJClass.m
    + (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key
    {
        NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
        if (array) return array;

        // 创建、存储
        [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];

        if ([self respondsToSelector:selector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            NSArray *subArray = [self performSelector:selector];
    #pragma clang diagnostic pop
            if (subArray) {
                [array addObjectsFromArray:subArray];
            }
        }

        [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
            NSArray *subArray = objc_getAssociatedObject(c, key);
            [array addObjectsFromArray:subArray];
        }];
        return array;
    }

根据上述的名单,来判断属性是否需要被忽略

    // 检测是否被忽略
    if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
    if ([ignoredPropertyNames containsObject:property.name]) return;

遍历成员变量

+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration遍历所有的属性。属性会先从缓存cachedProperties中取,如果存在,直接返回;如果不存在,就遍历类来获取属性列表,创建MJProperty对象,并设置它的name(属性名)、type(数据类型)、srcClass(来源类)等等;同时,还会设置propertyKeysDict(类对应的字典中的所有key)
objectClassInArrayDict(类对应的字典数组中的模型类)。

    // NSObject+MJProperty.m
    // 获得成员变量
    NSArray *cachedProperties = [self properties];

    // 遍历成员变量
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        enumeration(property, &stop);
        if (stop) break;
    }

---->

    + (NSMutableArray *)properties
    {
        NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];

        if (cachedProperties == nil) {
            cachedProperties = [NSMutableArray array];

            [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
                // 1.获得所有的成员变量
                unsigned int outCount = 0;
                objc_property_t *properties = class_copyPropertyList(c, &outCount);

                // 2.遍历每一个成员变量
                for (unsigned int i = 0; i<outCount; i++) {
                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];

                    NSLog(@"%@ - %@", property.name, property.srcClass);
                    // 过滤掉Foundation框架类里面的属性
                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                    property.srcClass = c;
                    [property setOriginKey:[self propertyKey:property.name] forClass:self];
                    [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
                    [cachedProperties addObject:property];
                }

                // 3.释放内存
                free(properties);
            }];

            [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
        }

        return cachedProperties;
    }    

在上述设置模型属性时,会调用mj_replacedKeyFromPropertyName查看是否需要替换为新的键值。通常,特殊字符如:iddescription不能作为对象属性名,所以如果确实需要该属性名时,可以用IDdesc等代替,然后就需要实现该方法做替换,使对象的ID属性名对应字典中的id键。

    [property setOriginKey:[self propertyKey:property.name] forClass:self];

---->

    // NSObject+MJProperty.m
    + (id)propertyKey:(NSString *)propertyName
    {
        ...
        // 查看有没有需要替换的key
        if ((!key || [key isEqual:propertyName]) && [self respondsToSelector:@selector(mj_replacedKeyFromPropertyName)]) {
            key = [self mj_replacedKeyFromPropertyName][propertyName];
        }
        ...
    }

取出字典中的属性值

根据变量名获取字典中对应的值

    // 取出属性值
    id value;
    NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
    for (NSArray *propertyKeys in propertyKeyses) {
        value = keyValues;
        for (MJPropertyKey *propertyKey in propertyKeys) {
            value = [propertyKey valueInObject:value];
        }
        if (value) break;
    }

过滤值

如果用户需要对取得的字典中的值进行特殊处理,则需要实现mj_newValueFromOldValue:property:方法,并返回对应key的新值。如修改日期格式等等

    // NSObject+MJKeyValue.m
    // 值的过滤
    id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
    if (newValue != value) { // 有过滤后的新值
        [property setValue:newValue forObject:self];
        return;
    }

---->

    // NSObject+MJProperty.m
    // 如果有实现方法
    if ([object respondsToSelector:@selector(mj_newValueFromOldValue:property:)]) {
        return [object mj_newValueFromOldValue:oldValue property:property];
    }
    // 兼容旧版本
    if ([self respondsToSelector:@selector(newValueFromOldValue:property:)]) {
        return [self performSelector:@selector(newValueFromOldValue:property:)  withObject:oldValue  withObject:property];
    }

    // 查看静态设置
    __block id newValue = oldValue;
    [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
        MJNewValueFromOldValue block = objc_getAssociatedObject(c, &MJNewValueFromOldValueKey);
        if (block) {
            newValue = block(object, oldValue, property);
            *stop = YES;
        }
    }];
    return newValue;    

获取变量的类型

根据property属性获取属性的类型(基本数据类型则为nil)和数组中的成员对象类型(如果属性不是数组,则为空对象类型)

    // 复杂处理
    MJPropertyType *type = property.type;
    Class propertyClass = type.typeClass;
    Class objectClass = [property objectClassInArrayForClass:[self class]];

转换为可变类型

如果对象的属性是可变类型,但是从字典中取出的值都是不可变类型,那么需要把取出的值转换为可变类型

    // 不可变 -> 可变处理
    if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
        value = [NSMutableArray arrayWithArray:value];
    } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
        value = [NSMutableDictionary dictionaryWithDictionary:value];
    } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
        value = [NSMutableString stringWithString:value];
    } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
        value = [NSMutableData dataWithData:value];
    }

模型属性

这里用到了是不是Foundation的判断,如果属性是自定义属性,就递归调用mj_objectWithKeyValues再次进行赋值

    if (!type.isFromFoundation && propertyClass) { // 模型属性
        value = [propertyClass mj_objectWithKeyValues:value context:context];
    }

数组属性

如果是NSURL的对象类型,就获取它的url字符串,然后组合成数组;如果是其它对象类型,就调用mj_objectArrayWithKeyValuesArray::转换为模型数组

    if (objectClass) {
        if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
            // string array -> url array
            NSMutableArray *urlArray = [NSMutableArray array];
            for (NSString *string in value) {
                if (![string isKindOfClass:[NSString class]]) continue;
                [urlArray addObject:string.mj_url];
            }
            value = urlArray;
        } else { // 字典数组-->模型数组
            value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
        }
    }

处理模型属性和数组属性之外的其它属性

如果属性的类型和字典中对应的值的类型不匹配,就进行转换,如:NSNumber -> NSStringNSURL -> NSStringNSString -> NSURL,
NSString -> NSURL。另外,针对属性是布尔类型,而值是字符串:yes/trueno/false(忽略大小写情况),则转换为YESNO的包装类型。最后,如果属性和值的类型依然不匹配,那么就把值设为空,给后面KVC使用

    if (propertyClass == [NSString class]) {
        if ([value isKindOfClass:[NSNumber class]]) {
            // NSNumber -> NSString
            value = [value description];
        } else if ([value isKindOfClass:[NSURL class]]) {
            // NSURL -> NSString
            value = [value absoluteString];
        }
    } else if ([value isKindOfClass:[NSString class]]) {
        if (propertyClass == [NSURL class]) {
            // NSString -> NSURL
            // 字符串转码
            value = [value mj_url];
        } else if (type.isNumberType) {
            NSString *oldValue = value;

            // NSString -> NSNumber
            if (type.typeClass == [NSDecimalNumber class]) {
                value = [NSDecimalNumber decimalNumberWithString:oldValue];
            } else {
                value = [numberFormatter_ numberFromString:oldValue];
            }

            // 如果是BOOL
            if (type.isBoolType) {
                // 字符串转BOOL(字符串没有charValue方法)
                // 系统会调用字符串的charValue转为BOOL类型
                NSString *lower = [oldValue lowercaseString];
                if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) {
                    value = @YES;
                } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
                    value = @NO;
                }
            }
        }
    }

    // value和property类型不匹配
    if (propertyClass && ![value isKindOfClass:propertyClass]) {
        value = nil;
    }

赋值

利用KVC赋值

    [property setValue:value forObject:self];

归档

MJExtensionCodingImplementation这个宏表明对象支持归档,通过分类支持NSCoding协议,实现归档和解档的方法,我们可以直接把对象归档和解档,十分方便

宏定义函数

这个宏可以构建错误日志并输出,方便调试。同时,对于宏替换不是十分了解的时候,可以使用预处理命令,把文件的宏全部替换后,就可以十分方便的看到宏被替换后的代码模样:

# clang -E 源文件 [-o 目标文件]

    // 判断参数是不是字典
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");

---->
    // MJExtensionConst.h
    #define MJExtensionAssertError(condition, returnValue, clazz, msg) \
    [clazz setMj_error:nil]; \
    if ((condition) == NO) { \
        MJExtensionBuildError(clazz, msg); \
        return returnValue;\
    }

2.具体的转换过程

在不考虑CoreData的情况下,我们直接来看转换的核心代码的方法,主要就是mj_setKeyValues:context:方法来完成字典转模型的工作

/**
 核心代码:
 */
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context

把传进来的数据转换为字典

可以看到,传进来的数据还可以是NSString
NSData类型,最后都被转换为JSON格式的数据

    // NSObject+MJKeyValue.m
    // 获得JSON对象
    keyValues = [keyValues mj_JSONObject];

--->

    if ([self isKindOfClass:[NSString class]]) {
        return [NSJSONSerialization JSONObjectWithData:[((NSString *)self) dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    } else if ([self isKindOfClass:[NSData class]]) {
        return [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
    }

    // 返回字典
    return self.mj_keyValues;

获取转换的白名单和黑名单

首先检查名单是不是存在,如果存在,就直接返回;不存在,则根据方法mj_totalAllowedPropertyNames来获取名单,然后返回

    // NSObject+MJKeyValue.m
    Class clazz = [self class];
    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

--->
    // NSObject+MJClass.m
    + (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key
    {
        NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
        if (array) return array;

        // 创建、存储
        [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];

        if ([self respondsToSelector:selector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            NSArray *subArray = [self performSelector:selector];
    #pragma clang diagnostic pop
            if (subArray) {
                [array addObjectsFromArray:subArray];
            }
        }

        [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
            NSArray *subArray = objc_getAssociatedObject(c, key);
            [array addObjectsFromArray:subArray];
        }];
        return array;
    }

根据上述的名单,来判断属性是否需要被忽略

    // 检测是否被忽略
    if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
    if ([ignoredPropertyNames containsObject:property.name]) return;

遍历成员变量

+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration遍历所有的属性。属性会先从缓存cachedProperties中取,如果存在,直接返回;如果不存在,就遍历类来获取属性列表,创建MJProperty对象,并设置它的name(属性名)、type(数据类型)、srcClass(来源类)等等;同时,还会设置propertyKeysDict(类对应的字典中的所有key)
objectClassInArrayDict(类对应的字典数组中的模型类)。

    // NSObject+MJProperty.m
    // 获得成员变量
    NSArray *cachedProperties = [self properties];

    // 遍历成员变量
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        enumeration(property, &stop);
        if (stop) break;
    }

---->

    + (NSMutableArray *)properties
    {
        NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];

        if (cachedProperties == nil) {
            cachedProperties = [NSMutableArray array];

            [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
                // 1.获得所有的成员变量
                unsigned int outCount = 0;
                objc_property_t *properties = class_copyPropertyList(c, &outCount);

                // 2.遍历每一个成员变量
                for (unsigned int i = 0; i<outCount; i++) {
                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];

                    NSLog(@"%@ - %@", property.name, property.srcClass);
                    // 过滤掉Foundation框架类里面的属性
                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                    property.srcClass = c;
                    [property setOriginKey:[self propertyKey:property.name] forClass:self];
                    [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
                    [cachedProperties addObject:property];
                }

                // 3.释放内存
                free(properties);
            }];

            [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
        }

        return cachedProperties;
    }    

在上述设置模型属性时,会调用mj_replacedKeyFromPropertyName查看是否需要替换为新的键值。通常,特殊字符如:iddescription不能作为对象属性名,所以如果确实需要该属性名时,可以用IDdesc等代替,然后就需要实现该方法做替换,使对象的ID属性名对应字典中的id键。

    [property setOriginKey:[self propertyKey:property.name] forClass:self];

---->

    // NSObject+MJProperty.m
    + (id)propertyKey:(NSString *)propertyName
    {
        ...
        // 查看有没有需要替换的key
        if ((!key || [key isEqual:propertyName]) && [self respondsToSelector:@selector(mj_replacedKeyFromPropertyName)]) {
            key = [self mj_replacedKeyFromPropertyName][propertyName];
        }
        ...
    }

取出字典中的属性值

根据变量名获取字典中对应的值

    // 取出属性值
    id value;
    NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
    for (NSArray *propertyKeys in propertyKeyses) {
        value = keyValues;
        for (MJPropertyKey *propertyKey in propertyKeys) {
            value = [propertyKey valueInObject:value];
        }
        if (value) break;
    }

过滤值

如果用户需要对取得的字典中的值进行特殊处理,则需要实现mj_newValueFromOldValue:property:方法,并返回对应key的新值。如修改日期格式等等

    // NSObject+MJKeyValue.m
    // 值的过滤
    id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
    if (newValue != value) { // 有过滤后的新值
        [property setValue:newValue forObject:self];
        return;
    }

---->

    // NSObject+MJProperty.m
    // 如果有实现方法
    if ([object respondsToSelector:@selector(mj_newValueFromOldValue:property:)]) {
        return [object mj_newValueFromOldValue:oldValue property:property];
    }
    // 兼容旧版本
    if ([self respondsToSelector:@selector(newValueFromOldValue:property:)]) {
        return [self performSelector:@selector(newValueFromOldValue:property:)  withObject:oldValue  withObject:property];
    }

    // 查看静态设置
    __block id newValue = oldValue;
    [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
        MJNewValueFromOldValue block = objc_getAssociatedObject(c, &MJNewValueFromOldValueKey);
        if (block) {
            newValue = block(object, oldValue, property);
            *stop = YES;
        }
    }];
    return newValue;    

获取变量的类型

根据property属性获取属性的类型(基本数据类型则为nil)和数组中的成员对象类型(如果属性不是数组,则为空对象类型)

    // 复杂处理
    MJPropertyType *type = property.type;
    Class propertyClass = type.typeClass;
    Class objectClass = [property objectClassInArrayForClass:[self class]];

转换为可变类型

如果对象的属性是可变类型,但是从字典中取出的值都是不可变类型,那么需要把取出的值转换为可变类型

    // 不可变 -> 可变处理
    if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
        value = [NSMutableArray arrayWithArray:value];
    } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
        value = [NSMutableDictionary dictionaryWithDictionary:value];
    } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
        value = [NSMutableString stringWithString:value];
    } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
        value = [NSMutableData dataWithData:value];
    }

模型属性

这里用到了是不是Foundation的判断,如果属性是自定义属性,就递归调用mj_objectWithKeyValues再次进行赋值

    if (!type.isFromFoundation && propertyClass) { // 模型属性
        value = [propertyClass mj_objectWithKeyValues:value context:context];
    }

数组属性

如果是NSURL的对象类型,就获取它的url字符串,然后组合成数组;如果是其它对象类型,就调用mj_objectArrayWithKeyValuesArray::转换为模型数组

    if (objectClass) {
        if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
            // string array -> url array
            NSMutableArray *urlArray = [NSMutableArray array];
            for (NSString *string in value) {
                if (![string isKindOfClass:[NSString class]]) continue;
                [urlArray addObject:string.mj_url];
            }
            value = urlArray;
        } else { // 字典数组-->模型数组
            value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
        }
    }

处理模型属性和数组属性之外的其它属性

如果属性的类型和字典中对应的值的类型不匹配,就进行转换,如:NSNumber -> NSStringNSURL -> NSStringNSString -> NSURL,
NSString -> NSURL。另外,针对属性是布尔类型,而值是字符串:yes/trueno/false(忽略大小写情况),则转换为YESNO的包装类型。最后,如果属性和值的类型依然不匹配,那么就把值设为空,给后面KVC使用

    if (propertyClass == [NSString class]) {
        if ([value isKindOfClass:[NSNumber class]]) {
            // NSNumber -> NSString
            value = [value description];
        } else if ([value isKindOfClass:[NSURL class]]) {
            // NSURL -> NSString
            value = [value absoluteString];
        }
    } else if ([value isKindOfClass:[NSString class]]) {
        if (propertyClass == [NSURL class]) {
            // NSString -> NSURL
            // 字符串转码
            value = [value mj_url];
        } else if (type.isNumberType) {
            NSString *oldValue = value;

            // NSString -> NSNumber
            if (type.typeClass == [NSDecimalNumber class]) {
                value = [NSDecimalNumber decimalNumberWithString:oldValue];
            } else {
                value = [numberFormatter_ numberFromString:oldValue];
            }

            // 如果是BOOL
            if (type.isBoolType) {
                // 字符串转BOOL(字符串没有charValue方法)
                // 系统会调用字符串的charValue转为BOOL类型
                NSString *lower = [oldValue lowercaseString];
                if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) {
                    value = @YES;
                } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
                    value = @NO;
                }
            }
        }
    }

    // value和property类型不匹配
    if (propertyClass && ![value isKindOfClass:propertyClass]) {
        value = nil;
    }

赋值

利用KVC赋值

    [property setValue:value forObject:self];

归档

MJExtensionCodingImplementation这个宏表明对象支持归档,通过分类支持NSCoding协议,实现归档和解档的方法,我们可以直接把对象归档和解档,十分方便

宏定义函数

这个宏可以构建错误日志并输出,方便调试。同时,对于宏替换不是十分了解的时候,可以使用预处理命令,把文件的宏全部替换后,就可以十分方便的看到宏被替换后的代码模样:

# clang -E 源文件 [-o 目标文件]

    // 判断参数是不是字典
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");

---->
    // MJExtensionConst.h
    #define MJExtensionAssertError(condition, returnValue, clazz, msg) \
    [clazz setMj_error:nil]; \
    if ((condition) == NO) { \
        MJExtensionBuildError(clazz, msg); \
        return returnValue;\
    }

通过重写方法返回数组,白名单和黑名单的数据的设置

NSObject+MJClass中提供了几个方法,分别如下:

//属性白名单配置(用于模型字典转化)
+ (NSMutableArray *)mj_totalAllowedPropertyNames;

//归档属性白名单配置
+ (NSMutableArray *)mj_totalIgnoredPropertyNames;

//属性黑名单配置(用于归档)
+ (NSMutableArray *)mj_totalAllowedCodingPropertyNames;

//归档属性黑名单配置
+ (NSMutableArray *)mj_totalIgnoredCodingPropertyNames;

以上四个方法,跟外部传入block返回数组来进行数据配置的情况一致,也是调用同一个方法mj_totalObjectsWithSelector: key:,只不过传入之后是这么操作:

+ (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key
{
    NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
    if (array) return array;

    // 创建、存储
    [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];

    if ([self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        NSArray *subArray = [self performSelector:selector];
#pragma clang diagnostic pop
        if (subArray) {
            [array addObjectsFromArray:subArray];
        }
    }

    [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
        NSArray *subArray = objc_getAssociatedObject(c, key);
        [array addObjectsFromArray:subArray];
    }];
    return array;
}

可以看到,内部调用主要是调用了这个方法:

+ (NSMutableDictionary *)dictForKey:(const void *)key
{
    @synchronized (self) {
        if (key == &MJAllowedPropertyNamesKey) return allowedPropertyNamesDict_;
        if (key == &MJIgnoredPropertyNamesKey) return ignoredPropertyNamesDict_;
        if (key == &MJAllowedCodingPropertyNamesKey) return allowedCodingPropertyNamesDict_;
        if (key == &MJIgnoredCodingPropertyNamesKey) return ignoredCodingPropertyNamesDict_;
        return nil;
    }
}

内部用于存储数据的四个字典:

static NSMutableDictionary *allowedPropertyNamesDict_;
static NSMutableDictionary *ignoredPropertyNamesDict_;
static NSMutableDictionary *allowedCodingPropertyNamesDict_;
static NSMutableDictionary *ignoredCodingPropertyNamesDict_;

@synchronized (self)
{}这个语法,相当于NSLock,为了避免多线程访问时的问题。更多戳这里

仔细的分析下这个方法mj_totalObjectsWithSelector: key:

  • 从关联属性中取出,如果有值的话,直接返回。(前提是外部调用了block的方法来设置黑白名单的方法,并传值进来)。
    对应代码:

NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
if (array) return array;
  • dictForKey没有返回值,说明外部没有通过block来设置名单。创建一个空的数组,并根据key值设置给四个字典中的对应的一个。通过respondsToSelector动态获取当前继承自NSObject的类中的重写的方法中返回的数组subArray,此时,该数组已经关联到其中一个字典了。
    对应代码:

// 创建、存储
    [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];

    if ([self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        NSArray *subArray = [self performSelector:selector];
#pragma clang diagnostic pop
        if (subArray) {
            [array addObjectsFromArray:subArray];
        }
    }
  • 通过上面的操作,外部重写方法所返回的数组,已经作为关联属性存储起来了,此时只需要通过objc_getAssociatedObject通过key值取出来,返回:

对应代码:

[self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
        NSArray *subArray = objc_getAssociatedObject(c, key);
        [array addObjectsFromArray:subArray];
}];
return array;
  • 稍微优化下,使用setValuesForKeysWithDictionary可以简化赋值代码如下。

MJExtension的分析到此结束,有不足之处,欢迎斧正。

参考资料

MJExtension源码解析

MJExtension源码阅读

跟着MJExtension实现简单的字典转模型框架

MJExtension的分析到此结束,有不足之处,欢迎斧正。

参考资料

MJExtension源码解析

MJExtension源码阅读

跟着MJExtension实现简单的字典转模型框架

MJExtension的分析到此结束,有不足之处,欢迎斧正。

参考资料

MJExtension源码解析

MJExtension源码阅读

跟着MJExtension实现简单的字典转模型框架

NSObject+MJCoding

NSObject+MJCoding.h

#import <Foundation/Foundation.h>
#import "MJExtensionConst.h"

/**
 *  Codeing协议
 */
@protocol MJCoding <NSObject>
@optional
/**
 *  这个数组中的属性名才会进行归档
 */
+ (NSArray *)mj_allowedCodingPropertyNames;
/**
 *  这个数组中的属性名将会被忽略:不进行归档
 */
+ (NSArray *)mj_ignoredCodingPropertyNames;
@end

@interface NSObject (MJCoding) <MJCoding>
/**
 *  解码(从文件中解析对象)
 */
- (void)mj_decode:(NSCoder *)decoder;
/**
 *  编码(将对象写入文件中)
 */
- (void)mj_encode:(NSCoder *)encoder;
@end

/**
 归档的实现
 */
#define MJCodingImplementation \
- (id)initWithCoder:(NSCoder *)decoder \
{ \
if (self = [super init]) { \
[self mj_decode:decoder]; \
} \
return self; \
} \
\
- (void)encodeWithCoder:(NSCoder *)encoder \
{ \
[self mj_encode:encoder]; \
}

#define MJExtensionCodingImplementation MJCodingImplementation

NSObject+MJCoding.h文件中看起来很长,其实分为三个部分:

  • MJCoding协议;

其中有两个方法:mj_allowedCodingPropertyNamesmj_ignoredCodingPropertyNames看注释可以得知,是对外暴露的关于黑白名单的设置,而且此分类已经帮我们引入了@interface NSObject (MJCoding) <MJCoding>,外部实现不再需要重复引入该协议,实现上述两个方法即可,当然,看业务需求,你是完全自由设置的。例如:

#import "Father.h"

@interface Son : Father
@property(nonatomic,copy)NSString *sonProperty;
@end

#import "Son.h"
#import <NSObject+MJCoding.h>

@implementation Son
//归档实现
MJCodingImplementation
//黑名单设置
+(NSArray*)mj_ignoredCodingPropertyNames{
    return @[@"sonProperty"];
}
@end
  • 解码、编码两个方法;

两个方法:mj_decodemj_encode其实都是通过mj_enumerateProperties遍历自定义对象的属性,内部已经实现多继承的属性对象遍历,然后逐个去对比黑白名单,从而得到需要编码和解码的属性。

- (void)mj_encode:(NSCoder *)encoder
{
    Class clazz = [self class];

    NSArray *allowedCodingPropertyNames = [clazz mj_totalAllowedCodingPropertyNames];
    NSArray *ignoredCodingPropertyNames = [clazz mj_totalIgnoredCodingPropertyNames];

    [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
        // 检测是否被忽略
        if (allowedCodingPropertyNames.count && ![allowedCodingPropertyNames containsObject:property.name]) return;
        if ([ignoredCodingPropertyNames containsObject:property.name]) return;

        id value = [property valueForObject:self];
        if (value == nil) return;
        [encoder encodeObject:value forKey:property.name];
    }];
}

- (void)mj_decode:(NSCoder *)decoder
{
    Class clazz = [self class];

    NSArray *allowedCodingPropertyNames = [clazz mj_totalAllowedCodingPropertyNames];
    NSArray *ignoredCodingPropertyNames = [clazz mj_totalIgnoredCodingPropertyNames];

    [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
        // 检测是否被忽略
        if (allowedCodingPropertyNames.count && ![allowedCodingPropertyNames containsObject:property.name]) return;
        if ([ignoredCodingPropertyNames containsObject:property.name]) return;

        id value = [decoder decodeObjectForKey:property.name];
        if (value == nil) { // 兼容以前的MJExtension版本
            value = [decoder decodeObjectForKey:[@"_" stringByAppendingString:property.name]];
        }
        if (value == nil) return;
        [property setValue:value forObject:self];
    }];
}
  • 解码编码两个方法的宏抽取。

说到底,mj_decodemj_encode其实都是对实现系统NSCodingd
的两个方法做了封装,最终还是需要写到-(instancetype)initWithCoder:(NSCoder *)aDecoder-(void) encodeWithCoder:(NSCoder *)aCoder的内部,不过对于每个需要归档解档的Class来说,代码都是一致的,所以把这一致的抽取成一句宏,之后调用,就只需要调用这一句了,MJExtensionCodingImplementationMJCodingImplementation都可以。具体实现:

#define MJCodingImplementation \
- (id)initWithCoder:(NSCoder *)decoder \
{ \
if (self = [super init]) { \
[self mj_decode:decoder]; \
} \
return self; \
} \
\
- (void)encodeWithCoder:(NSCoder *)encoder \
{ \
[self mj_encode:encoder]; \
}

#define MJExtensionCodingImplementation MJCodingImplementation

最后

其中应该需要补充,有些解释不太清楚,后面会做补充。

如果您觉得本文对您有一定的帮助,请随手点个喜欢,十分感谢!🌹🌹🌹

// 1.获取json数据NSString *json = @"{\"name\" : \"idhong\", \"age\" : 15}";// 2.json->NSDictionaryNSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:NULL];// 3.创建userBMUser *user = [BMUser new];// 4.赋值[user setValuesForKeysWithDictionary:jsonDict]; 

如果属性特别多,而且模型嵌套模型时手写就特别麻烦而且容易出错,其中至少包括如下问题;

  • 1.模型的属性是另一个模型
  • 2.模型的属性是数组(数组中的对象又是模型)
  • 3.以及相互嵌套等。
  • 使用setValuesForKeysWithDictionary无法解决问题,同时手动编码无法想象。

归根结底 json -> Model 主要是以下3步

  • 1.创建模型

  • 2.为模型中的属性赋值

  • 3.返回模型

  • 其中最核心的就是第2步,怎么样动态的为模型赋值,当然是使用Runtime,MJExtension
    算是使用率较多的一个库,下面就简单分析下其实现原理吧。

  • 1.使用Runtime动态获取模型的属性(包括继承链的所有属性,MJExtension对模型的属性做了缓存,下次转换时自己使用,空间换时间)

  • 2.又第1步获取到的属性可以解析出各属性的全部信息(包括属性类型,属性修饰符等参考这里
    )

  • 3.第2步获取属性后,取到属性名在去NSDictionary)中获出相应的值

  • 4.在第3步取出的值使用KVC设置给Model即可(MJExtension使用KVC赋值,YYModel底层是直接使用objc_msgSend)

  • 5.所以为什么如果模型中有属性是数组,数组中又是模型时,需要我们指定其类型,才可为我们主动将其转换。

图片 2

怎样自己实现MJExtension

  • 动态获取模型属性
  • 解析属性相关信息
  • 如果是数组类型,因为不知道数组中应该存放具体的类型,所以需提供方法让使用者提供具体的类型
  • 使用KVC赋值
  • 其他扩展信息,包括黑名单,白名单,换属性名,转换结束等等扩展信息。

  • 引用一张图

图片 3

MJExtension 一共21个文件,主要用3个类,5个分类

  • MJExtension.h头文件
  • MJExtensionConst 一些常量,宏,版本控制等
  • MJFoundation主要是提供判断是否为Foundation框架的类
  • MJProperty封装属性对象(objc_property_t内部包括属性名称,属性类型等相关信息)
  • MJPropertyKey 属性的key
  • MJPropertyType
    属性类型(类型标识符这里,是否id,是否基本数据类型等)
  • NSString+MJExtension字符串的一些扩展(包括下划线,大小写等处理)
  • NSObject+MJProperty主要提供遍历一个类的属性列表
  • NSObject+MJCoding提供归档功能
  • NSObject+MJClass提供扩展功能,黑名单,白名单,数组中的类型自定义等。

  • 我们使用最基本的dict->Model方法是:

+ (instancetype)mj_objectWithKeyValues:keyValues;

其实现在NSObject+MJKeyValue.m的203行如下

+ (instancetype)mj_objectWithKeyValues:keyValues{ return [self mj_objectWithKeyValues:keyValues context:nil];}

内部调用了下面的方法

+ (instancetype)mj_objectWithKeyValues:keyValues context:(NSManagedObjectContext *)context{ // 获得JSON对象 keyValues = [keyValues mj_JSONObject]; MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], nil, [self class], @"keyValues参数不是一个字典"); if ([self isSubclassOfClass:[NSManagedObject class]] && context) { NSString *entityName = [NSStringFromClass componentsSeparatedByString:@"."].lastObject; return [[NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context] mj_setKeyValues:keyValues context:context]; } return [[[self alloc] init] mj_setKeyValues:keyValues];}

首先是mj_JSONObject方法,其作用是把NSData或者NSString为NSDictionary类型,因为上面的方法支持NSDictionary,NSString,NSData类型,其具体实现就不在赘述了。

这里先不考虑CoreData及NSManagedObject

然后是创建模型同时调用赋值属性

[[[self alloc] init] mj_setKeyValues:keyValues]

最后调到

- (instancetype)mj_setKeyValues:keyValues context:(NSManagedObjectContext *)context

此方法是整个库的核心代码部分。

- (instancetype)mj_setKeyValues:keyValues context:(NSManagedObjectContext *)context{ // 获得JSON对象 keyValues = [keyValues mj_JSONObject]; MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典"); Class clazz = [self class]; NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames]; NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames]; //通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。 [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) { @try { // 0.检测是否被忽略 if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return; if ([ignoredPropertyNames containsObject:property.name]) return; // 1.取出属性值 id value; NSArray *propertyKeyses = [property propertyKeysForClass:clazz]; for (NSArray *propertyKeys in propertyKeyses) { value = keyValues; for (MJPropertyKey *propertyKey in propertyKeys) { value = [propertyKey valueInObject:value]; } if  break; } // 值的过滤 id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property]; if (newValue != value) { // 有过滤后的新值 [property setValue:newValue forObject:self]; return; } // 如果没有值,就直接返回 if (!value || value == [NSNull null]) return; // 2.复杂处理 MJPropertyType *type = property.type; Class propertyClass = type.typeClass; Class objectClass = [property objectClassInArrayForClass:[self class]]; // 不可变 -> 可变处理 if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) { value = [NSMutableArray arrayWithArray:value]; } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) { value = [NSMutableDictionary dictionaryWithDictionary:value]; } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) { value = [NSMutableString stringWithString:value]; } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) { value = [NSMutableData dataWithData:value]; } if (!type.isFromFoundation && propertyClass) { // 模型属性 value = [propertyClass mj_objectWithKeyValues:value context:context]; } else if (objectClass) { if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) { // string array -> url array NSMutableArray *urlArray = [NSMutableArray array]; for (NSString *string in value) { if (![string isKindOfClass:[NSString class]]) continue; [urlArray addObject:string.mj_url]; } value = urlArray; } else { // 字典数组-->模型数组 value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context]; } } else { if (propertyClass == [NSString class]) { if ([value isKindOfClass:[NSNumber class]]) { // NSNumber -> NSString value = [value description]; } else if ([value isKindOfClass:[NSURL class]]) { // NSURL -> NSString value = [value absoluteString]; } } else if ([value isKindOfClass:[NSString class]]) { if (propertyClass == [NSURL class]) { // NSString -> NSURL // 字符串转码 value = [value mj_url]; } else if (type.isNumberType) { NSString *oldValue = value; // NSString -> NSNumber if (type.typeClass == [NSDecimalNumber class]) { value = [NSDecimalNumber decimalNumberWithString:oldValue]; } else { value = [numberFormatter_ numberFromString:oldValue]; } // 如果是BOOL if (type.isBoolType) { // 字符串转BOOL(字符串没有charValue方法) // 系统会调用字符串的charValue转为BOOL类型 NSString *lower = [oldValue lowercaseString]; if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) { value = @YES; } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) { value = @NO; } } } } // value和property类型不匹配 if (propertyClass && ![value isKindOfClass:propertyClass]) { value = nil; } } // 3.赋值 [property setValue:value forObject:self]; } @catch (NSException *exception) { MJExtensionBuildError([self class], exception.reason); MJExtensionLog(@"%@", exception); } }]; // 转换完毕 if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) { [self mj_keyValuesDidFinishConvertingToObject]; } return self;}

获取模型可以转换和忽略的属性

NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

此方法在调用时,这里做了缓存处理,只会访问一次,同时会调到外层的使用者实现的方法中

然后调用

+ mj_enumerateProperties:(MJPropertiesEnumeration)enumeration

遍历所有的属性,此方法同样做了缓存处理,如果某Class遍历过,此库内部会把其数组缓存起来,下次直接遍历即可

Block回调中可以获取到每一个MJProperty

几步比较常规的操作,在库用用详细的注释

  • 检测是否被忽略
  • 取出属性值
  • 值的过滤(提供了尾部可以对特殊值做处理的功能)

复杂类型处理

实现是对NSArray->NSMutableArray,NSData->NSMutableData,NSDictionary->NSMutableDictionary做可变处理。

因为对复杂类型需继续继续转换。

如果是模型( !type.isFromFoundation && propertyClass )就重复最开始的方法

如果是NSURL 就把NSString转换为NSURL

如果是 字典数组-由继续最开始的方法调用

NSNumber类型

BOOL等类型的转换

最后使用KVC赋值,当然在内部做了是否支持KVC的判断。

// 3.赋值[property setValue:value forObject:self];

综上所述,MJExtension的底层实现还算常规,动态获取属性
属性的类型判断 必要的缓存 KVC赋值 其他性细节优化和扩展

有时间分析下YYModel,其使用到了CoreFoundationobjc_msgSend

发表评论

电子邮件地址不会被公开。 必填项已用*标注