iOS-三方库-MJExtension源码分析

我们经常需要从网络上拉取json数据,然后将json数据转化为自己的模型数据,将json数据转化为我们自己的模型数据经常使用的框架有YYModel和MJExtension,所以现在也是打算花一些时间看一下MJExtension的源码,并且写一篇博客记录一下,因为不记录下来的话感觉很容易忘,学习效果不佳。

一. MJExtension的使用

1. 最简单的使用

模型:

//User.h
@interface User : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *icon;
@property (nonatomic, assign)unsigned int age;
@property (nonatomic, copy)NSString *height;
@property (nonatomic, strong)NSNumber *money;

@end

字典转模型:

//ViewController.m
NSDictionary *dict = @{
                           @"name" : @"Jack",
                           @"icon" : @"lufy.png",
                           @"age" : @20,
                           @"height" : @"1.55",
                           @"money" : @100.9
                           };

// JSON -> User
User *user = [User mj_objectWithKeyValues:dict];
    
NSLog(@"name=%@, icon=%@, age=%u, height=%@, money=%@", user.name, user.icon, user.age, user.height, user.money);

打印结果:

name=Jack, icon=lufy.png, age=20, height=1.55, money=100.9

通过一句简单的代码,就把字典数据转化为了模型数据,非常方便简洁。

2. 复杂一点的使用

很多时候json转模型都不是这样简单,有时候会出现模型中嵌套模型或者模型中的属性名和json数据中的key不一致的情况。

下面看一下一个Student类的模型:

//Student.h
@interface Student : NSObject

@property (nonatomic, copy)NSString *ID;
@property (nonatomic, copy)NSString *desc;
@property (nonatomic, copy)NSString *nowName;
@property (nonatomic, copy)NSString *oldName;
@property (nonatomic, copy)NSString *nameChangedTime;
@property (nonatomic, strong)Bag *bag;

@end

我们看到Student模型中嵌套着Bag这个模型:

//Bag.h
@interface Bag : NSObject

@property (nonatomic, copy)NSString *name;
@property ( nonatomic, assign)double *price;

@end

然后我们再看一下json数据:

NSDictionary *dict = @{
                           @"id" : @"20",
                           @"description" : @"kids",
                           @"name" : @{
                                   @"newName" : @"lufy",
                                   @"oldName" : @"kitty",
                                   @"info" : @[
                                           @"test-data",
                                           @{
                                               @"nameChangedTime" : @"2013-08"
                                               }
                                           ]
                                   },
                           @"other" : @{
                                   @"bag" : @{
                                           @"name" : @"a red bag",
                                           @"price" : @100.7
                                           }
                                   }
                           };

可以看到字典数据中是id,而模型中是ID,同样也有desc和description,模型中有newName和oldName这些属性,而字典中这些属性在name字段下面,而且nameChangedTime层级也不一样,bag属性也是一样的道理,那么怎么办呢?

我们只需要让模型遵守MJKeyValue协议,实现+ (NSDictionary *)mj_replacedKeyFromPropertyName;协议方法,如下:

//Student.m
/**
 *  将属性名换为其他key去字典中取值
 *
 *  @return 字典中的key是属性名,value是从字典中取值用的key
 */
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
    return @{
             @"ID" : @"id",
             @"desc" : @"description",
             @"oldName" : @"name.oldName",
             @"nowName" : @"name.newName",
             @"nameChangedTime" : @"name.info[1].nameChangedTime",
             @"bag" : @"other.bag"
             };
}

这个方法的作用就是在给模型赋值的时候,把右边字段的值赋给模型中左边字段的属性

转化一下试试:

// JSON -> Student
Student *stu = [Student mj_objectWithKeyValues:dict];

// Printing
NSLog(@"ID=%@, desc=%@, oldName=%@, nowName=%@, nameChangedTime=%@",
      stu.ID, stu.desc, stu.oldName, stu.nowName, stu.nameChangedTime);
// ID=20, desc=kids, oldName=kitty, nowName=lufy, nameChangedTime=2013-08

NSLog(@"bagName=%@, bagPrice=%d", stu.bag.name, stu.bag.price);
// bagName=a red bag, bagPrice=100.700000

这里需要注意一个地方就是模型中的nameChangedTime这个属性,在字典中去取值的时候是取name.info[1].nameChangedTime这个字段的值,这个在后面我们讲核心源码的时候会用到。后面讲源码也会以上面这个为例子来讲,这样比较好理解。

二. MJExtension核心类简介

1. MJFoundation

//判断一个类是否是foundation类及其子类
+ (BOOL)isClassFromFoundation:(Class)c;
//判断属性是否是协议里面定义的
+ (BOOL)isFromNSObjectProtocolProperty:(NSString *)propertyName;

2. MJProperty

这个类非常重要,这个类是对我们类中属性的再封装

首先会通过runtime的方法去遍历类中的属性:

unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Student class], &count);
for (int i = 0; i < count; i++) {
    objc_property_t property = propertyList[i];
    const char *propertyName = property_getName(property);
    const char *attris = property_getAttributes(property);
    NSLog(@"%s %s", propertyName, attris);
}

free(propertyList);

打印结果:

ID T@"NSString",C,N,V_ID
desc T@"NSString",C,N,V_desc
nowName T@"NSString",C,N,V_nowName
oldName T@"NSString",C,N,V_oldName
nameChangedTime T@"NSString",C,N,V_nameChangedTime
bag T@"Bag",&,N,V_bag

通过char类型的attris字符串我们可以看到,它中间有一个串是表示它是属于哪一个类的,比如NSString,Bag。

通过遍历类的属性,我们得到了objc_property_t类型的属性对象,然后使用这个objc_property_t对象来创建一个对应的MJProperty对象,我们看看MJ大神是怎么做的:

#pragma mark - 缓存
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    //这里的self是MJProperty类对象
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        //给MJProperty类对象添加关联对象,使用property作为key,关联propertyObj实例对象
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

首先MJ大神通过objc_property_t对象这个key去缓存中取,如果缓存中取不到,那么就根据objc_property_t来创建一个MJProperty对象,并且把这个MJProperty对象通过property这个key与MJProperty类对象关联起来。那么下次如果再从缓存中取同一个objc_property_t对应的MJProperty对象就能取到了,就不用再创建了。这也是MJ大神使用缓存的一个地方

上面代码块中propertyObj.property = property;这行代码触发了MJProperty对象的setter方法:

设置属性.png

MJProperty有一个type属性,这个属性是MJPropertyType类的,就是表示MJProperty对象的property属性是属于什么类型的,具体可看源码。

另外每一个MJProperty对象还持有着两个字典,一个是propertyKeysDict,一个是objectClassInArrayDict。

  • propertyKeysDict
    这个字典的key是NSStringFromClass(class),值是一个数组,比如在复杂一点的应用中,给模型中的nameChangedTime这个属性赋值的时候,在字典中去取值的时候要对应name.info[1].nameChangedTime这个字段的值。那么就要把name,info,1,nameChangedTim,这个四个字段分别封装为一个MJPropertyKey,加入一个数组中,作为value。这个数组在最终取值的时候会用到。

  • objectClassInArrayDict
    这个字典的key也是NSStringFromClass(class),值是一个类对象,表示如果这个MJProperty对象的类型是数组,并且数组中的元素类型是模型,那么这个字典的value就是模型的类对象。

上面两个字典的解释如果没看懂也没关系,接着往下看,看完核心源码分析就懂了。

3. MJPropertyKey

上面说过,给模型中的nameChangedTime这个属性赋值的时候,在字典中取值的时候要对应name.info[1].nameChangedTime这个字段的值,那么就要把name,info,1,nameCHangedTime这四个字段分别封装成一个MJPropertyKey。

它有两个属性,一个属性是name,也就是name、info、1这种,还有一个属性就是type,它是自定义的MJPropertyKeyType类型的枚举值,这个枚举值有两种类型,即MJPropertyKeyTypeDictionary和MJPropertyKeyTypeArray,像name、info这种就属于MJPropertyKeyTypeDictionary类型的,1就属于MJPropertyKeyTypeArray类型的。这个也是在取值的时候用的,类型是MJPropertyKeyTypeDictionary就是从字典中取值,类型是MJPropertyKeyTypeArray就是从数组中取值。

4. MJPropertyType

MJProperty类有一个属性是type,这个属性是MJPropertyType类的,这个type属性就是表征这个MJProperty对象它的property属性属于什么类,NSString类或者NSNumber类等等。MJProperty对象的type是通过截取property的attributes得到code然后初始化为MJPropertyType对象得到的:

_type = [MJPropertyType cachedTypeWithCode:code];

三. 核心源码分析

我们就从复杂一点的使用这个例子去看一下MJExtension的核心源码。

沿着+ (instancetype)mj_objectWithKeyValues:(id)keyValues;这个方法一直往下查找就能找到其核心代码:

/**
 核心代码:
 */
- (instancetype)mj_setKeyValues:(id)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 (value) 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 {
                            NSDecimalNumber *decimalValue = [NSDecimalNumber decimalNumberWithString:oldValue];
                            value = decimalValue == [NSDecimalNumber notANumber] ? @(0) : @(decimalValue.doubleValue);
                        }
                        
                        // 如果是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;
                            }
                        }
                    }
                } else if ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){
                    // 过滤 NSDecimalNumber类型
                    if (![value isKindOfClass:[NSDecimalNumber class]]) {
                        value = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]];
                    }
                }
                
                // 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_didConvertToObjectWithKeyValues:)]) {
        [self mj_didConvertToObjectWithKeyValues:keyValues];
    }
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) {
        [self mj_keyValuesDidFinishConvertingToObject];
    }
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject:)]) {
        [self mj_keyValuesDidFinishConvertingToObject:keyValues];
    }
#pragma clang diagnostic pop
    return self;
}

这一部分代码很长,我们一点一点来看:

1. 将json数据转化为foundation类型

// 获得JSON对象
keyValues = [keyValues mj_JSONObject];

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

mj_JSONObject方法:

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

2. 获取白名单和黑名单

//类对象
Class clazz = [self class];
//白名单
NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
//黑名单
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];

allowedPropertyNames是允许进行字典和模型转换的属性名数组,ignoredPropertyNames是不允许进行字典和模型转换额属性名数组,要求自己的模型类遵守MJKeyValue协议,实现协议的如下方法:

/**
 *  只有这个数组中的属性名才允许进行字典和模型的转换
 */
+ (NSArray *)mj_allowedPropertyNames;

/**
 *  这个数组中的属性名将会被忽略:不进行字典和模型的转换
 */
+ (NSArray *)mj_ignoredPropertyNames;

然后这俩数组就是在上面协议方法中获取的。

3. 遍历整个类的属性

//通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法
+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration
{
    // 获得成员变量
    MJExtensionSemaphoreCreate
    MJExtensionSemaphoreWait
    NSArray *cachedProperties = [self mj_properties];
    MJExtensionSemaphoreSignal
    // 遍历成员变量
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        //回调block
        enumeration(property, &stop);
        if (stop) break;
    }
}

上面代码,先获取类中所有的属性,然后使用for循环遍历每一个属性,然后执行回调block。

先看一下+ (NSMutableArray *)properties;方法,代码如下:

+ (NSMutableArray *)mj_properties
{
    //先从缓存中获取成员变量
    NSMutableArray *cachedProperties = [self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
    if (cachedProperties == nil) {
    
        //缓存中没有就遍历类中的每一个成员变量
        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++) {
                    //遍历,然后根据OC的属性创建MJProperty
                    //给MJProperty类对象添加关联对象,objc_propert作为key,MJProperty实例对象作为关联的对象
                    MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
                    // 过滤掉Foundation框架类里面的属性
                    if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                    // 过滤掉`hash`, `superclass`, `description`, `debugDescription`
                    if ([MJFoundation isFromNSObjectProtocolProperty:property.name]) continue;
                    
                    property.srcClass = c;
                    //很重要的两个方法
                    [property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
                    [property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];
                    [cachedProperties addObject:property];
                }
                
                // 3.释放内存
                free(properties);
            }];
            
            //将获取的cachedProperties缓存下来
            [self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
        }
    }
    
    return cachedProperties;
}

上面代码,先从缓存中获取属性列表,如果缓存中没有,就遍历类中的每一个属性,拿到属性列表之后再存到缓存。核心代码就是获取类中的所有成员变量,然后包装成MJProperty对象,如下:

[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++) {
        //遍历,然后根据OC的属性创建MJProperty
        //给MJProperty类对象添加关联对象,objc_propert作为key,MJProperty实例对象作为关联的对象
        MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
        // 过滤掉Foundation框架类里面的属性
        if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
        // 过滤掉`hash`, `superclass`, `description`, `debugDescription`
        if ([MJFoundation isFromNSObjectProtocolProperty:property.name]) continue;
        
        property.srcClass = c;
        //很重要的两个方法
        [property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
        [property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];
        [cachedProperties addObject:property];
    }
    
    // 3.释放内存
    free(properties);
}];

首先通过+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration这个方法去遍历当前模型类及其父类,当追溯到Foundation类型的类时就停止遍历,方法实现如下:

+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration
{
    // 1.没有block就直接返回
    if (enumeration == nil) return;
    
    // 2.停止遍历的标记
    BOOL stop = NO;
    
    // 3.当前正在遍历的类
    Class c = self;
    
    // 4.开始遍历每一个类
    while (c && !stop) {
        // 4.1.执行操作
        enumeration(c, &stop);
        
        // 4.2.获得父类
        c = class_getSuperclass(c);
        
        if ([MJFoundation isClassFromFoundation:c]) break;
    }
}

比如有一个Person类,其有两个属性name和sex,有一个Student类是继承自Person类的,这个Student类自己有一个school属性。那么当我们使用runtime的方法读取Student类的属性列表时,只能读取到一个自己声明的属性school。但是实际上name和sex也是它的属性,所以这个时候就要遍历其父类,拿到所有的属性。

当我们拿到模型类的objc_property_t类型的属性时,就通过+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property方法将其封装成MJProperty对象:

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    //这里的self是MJProperty类对象
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        //给MJProperty类对象添加关联对象,使用property作为key,关联propertyObj实例对象
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

如上,这个方法先尝试从关联属性中通过property对象这个key来取出MJProperty对象,如果取不到就创建一个MJProperty对象,并通过property这个key将其与MJProperty的类对象关联起来,这样下次就可以直接通过关联属性来得到MJProperty的值了。然后再通过propertyObj.property = property;这行代码触发set方法,在set方法里面为MJProperty对象的name属性和type属性赋值,其中type属性就是和MJProperty对象关联的property属于什么类,是NSNumber类还是BOOL类等等:

- (void)setProperty:(objc_property_t)property
{
    _property = property;
    
    MJExtensionAssertParamNotNil(property);
    
    // 1.属性名
    _name = @(property_getName(property));
    
    // 2.成员类型
    NSString *attrs = @(property_getAttributes(property));
    NSUInteger dotLoc = [attrs rangeOfString:@","].location;
    NSString *code = nil;
    NSUInteger loc = 1;
    if (dotLoc == NSNotFound) { // 没有,
        code = [attrs substringFromIndex:loc];
    } else {
        code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)];
    }
    _type = [MJPropertyType cachedTypeWithCode:code];
}

下面两行代码非常重要

[property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
[property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];

对于第一行代码

+ (id)propertyKey:(NSString *)propertyName这个方法是获取模型的属性名在字典中对应的key,什么意思呢?还是拿第二个例子来说,它有一个nameChangedTime属性,由于我们在模型类中实现了+ (NSDictionary *)mj_replacedKeyFromPropertyName这个方法,且这个方法中与nameChangedTime相对应的是name.info[1].nameChangedTime,所以+ (id)propertyKey:(NSString *)propertyName返回的就是name.info[1].nameChangedTime这个字符串。

对于- (void)setOriginKey:(id)originKey forClass:(Class)c方法,这个方法会把name.info[1].nameChangedTime这个字符串拆解成一段一段,并封装成一个个MJPropertyKey对象,组成数组,赋值给MJProperty的propertyKeysDict这个字典:

setOriginKey forClass.png

- (void)setPorpertyKeys:(NSArray *)propertyKeys forClass:(Class)c方法的实现如下:

/** 对应着字典中的多级key */
- (void)setPorpertyKeys:(NSArray *)propertyKeys forClass:(Class)c
{
    if (propertyKeys.count == 0) return;
    NSString *key = NSStringFromClass(c);
    if (!key) return;
    
    MJ_LOCK(self.propertyKeysLock);
    self.propertyKeysDict[key] = propertyKeys;
    MJ_UNLOCK(self.propertyKeysLock);
}

对于第二行代码

如果模型中有数组类型的属性,并且数组中的元素也是模型类,那么就需要模型类遵守MJKeyValue协议,实现+ (NSDictionary *)mj_objectClassInArray;协议方法,就像这样:模型类中有一个数组类型的属性statuses,数组中的元素类型是模型,模型类是Status;另一个数组类型的属性是ads,数组中的元素类型是模型,模型类是Ad,如下:

+ (NSDictionary *)mj_objectClassInArray
{
    return @{
             @"statuses" : @"Status",
             @"ads" : @"Ad"
             };
}

这时如果在+ (Class)propertyObjectClassInArray:(NSString *)propertyName方法中传入statuses属性,那么返回的就是Status类,如下:

propertyObjectClassInArray.png

然后- (void)setObjectClassInArray:(Class)objectClass forClass:(Class)c方法将这个Status类对象赋值给MJProperty对象的objectClassInArrayDict字典。

最后再通过+ (NSMutableDictionary *)mj_propertyDictForKey:(const void *)key方法将获取的cachedProperties缓存下来,如下:

//将获取的cachedProperties缓存下来
[self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;

方法实现如下:

+ (NSMutableDictionary *)mj_propertyDictForKey:(const void *)key
{
    //静态变量
    static NSMutableDictionary *replacedKeyFromPropertyNameDict;
    static NSMutableDictionary *replacedKeyFromPropertyName121Dict;
    static NSMutableDictionary *newValueFromOldValueDict;
    static NSMutableDictionary *objectClassInArrayDict;
    static NSMutableDictionary *cachedPropertiesDict;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        replacedKeyFromPropertyNameDict = [NSMutableDictionary dictionary];
        replacedKeyFromPropertyName121Dict = [NSMutableDictionary dictionary];
        newValueFromOldValueDict = [NSMutableDictionary dictionary];
        objectClassInArrayDict = [NSMutableDictionary dictionary];
        cachedPropertiesDict = [NSMutableDictionary dictionary];
    });
    
    if (key == &MJReplacedKeyFromPropertyNameKey) return replacedKeyFromPropertyNameDict;
    if (key == &MJReplacedKeyFromPropertyName121Key) return replacedKeyFromPropertyName121Dict;
    if (key == &MJNewValueFromOldValueKey) return newValueFromOldValueDict;
    if (key == &MJObjectClassInArrayKey) return objectClassInArrayDict;
    if (key == &MJCachedPropertiesKey) return cachedPropertiesDict;
    return nil;
}

上面static的静态字典,保证程序运行中,字典会一直存在内存中,从而达到缓存的目的。

到这里遍历类的所有属性就结束了,这样获得了整个类的所有属性,每个属性被封装成了一个MJProperty对象,MJProperty对象有一个property属性,还有type属性来表征这个属性属于什么类。此外MJProperty对象还保存着两个字典propertyKeysDict和objectClassInArrayDict,这两个字典的key都是NSStringFromClass(c),前者的value是一个数组,这个数组里面的元素是MJPropertyKey类型的,主要是用来取值用的,后者的value是一个类对象,如果属性是一个数组类型的属性,且数组元素是模型类型,那么这个值就是模型的类对象。

4. 对模型进行赋值

对模型进行赋值是在+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration方法的回调block里一个一个进行的,我们一行一行的看回调block。

① 检测是否被忽略

如果这个属性不在属性白名单里或者在属性黑名单里,那么就返回,不对属性赋值:

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

② 取出属性值

从每个属性的propertyKeysDict字典中取出propertyKeys数组,根据propertyKeys数组来取值:

// 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 (value) break;
}

我们看一下- (id)valueInObject:(id)object这个方法是怎么操作的:

valueInObject.png

③ 值的过滤

// 值的过滤
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;

想要明白上面的代码就要明白MJKeyValue协议的如下方法:

/**
 *  旧值换新值,用于过滤字典中的值
 *
 *  @param oldValue 旧值
 *
 *  @return 新值
 */
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property;

MJExtension官方的实例如下:NSString -> NSDate, nil -> @""【过滤字典的值(比如字符串日期处理为NSDate、字符串nil处理为@"")】

// Book
#import "MJExtension.h"

@implementation Book
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property
{
    if ([property.name isEqualToString:@"publisher"]) {
        if (oldValue == nil) return @"";
    } else if (property.type.typeClass == [NSDate class]) {
        NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
        fmt.dateFormat = @"yyyy-MM-dd";
        return [fmt dateFromString:oldValue];
    }

    return oldValue;
}
@end

// NSDictionary
NSDictionary *dict = @{
                       @"name" : @"5分钟突破iOS开发",
                       @"publishedTime" : @"2011-09-10"
                       };
// NSDictionary -> Book
Book *book = [Book mj_objectWithKeyValues:dict];

// printing
NSLog(@"name=%@, publisher=%@, publishedTime=%@", book.name, book.publisher, book.publishedTime);

比如,我们需要将服务端返回的字符串日期处理为NSDate、字符串nil处理为@"",就按照上面的逻辑处理就好了。现在你应该就明白值的过滤是做什么的了。

④ 不可变 -> 可变处理

如果属性的类型是可变的类型,而取出的value是不可变的类型,那么就要把不可变类型变换为可变的类型:

// 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];
}

⑤ 复杂处理

上面就是完成了对属性的第一步赋值,但是这还不够,如果这个属性是模型类型,那么还要对这个模型再进行一次字典转模型操作。如果这个属性是数组类型且数组元素是模型类型,那么还要进行字典数组转模型数组的操作。或者属性是NSURL类型,value是NSString类型,这样也要进行一下转换:

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 {
                NSDecimalNumber *decimalValue = [NSDecimalNumber decimalNumberWithString:oldValue];
                value = decimalValue == [NSDecimalNumber notANumber] ? @(0) : @(decimalValue.doubleValue);
            }
            
            // 如果是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;
                }
            }
        }
    } else if ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){
        // 过滤 NSDecimalNumber类型
        if (![value isKindOfClass:[NSDecimalNumber class]]) {
            value = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]];
        }
    }
    
    // value和property类型不匹配
    if (propertyClass && ![value isKindOfClass:propertyClass]) {
        value = nil;
    }
}

⑥ 赋值

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

使用KVC赋值,如下:

/**
 *  设置成员变量的值
 */
- (void)setValue:(id)value forObject:(id)object
{
    if (self.type.KVCDisabled || value == nil) return;
    [object setValue:value forKey:self.name];
}

⑦ 转换完毕

转换完毕,回调MJKeyValue协议方法,返回self,如下:

    // 转换完毕
    if ([self respondsToSelector:@selector(mj_didConvertToObjectWithKeyValues:)]) {
        [self mj_didConvertToObjectWithKeyValues:keyValues];
    }
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) {
        [self mj_keyValuesDidFinishConvertingToObject];
    }
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject:)]) {
        [self mj_keyValuesDidFinishConvertingToObject:keyValues];
    }
#pragma clang diagnostic pop
    return self;

这里,整个模型赋值的过程也就完成了。

四. MJExtension中的一些缓存操作

MJExtension中进行了大量的缓存操作来优化性能,下面讲几个比较重要的缓存,理解了这些缓存也有助于更深入的理解整个框架。

  1. NSObject+MJProperty这个分类中保存着一个字典cachedPropertiesDict,这个字典的key是NSStringFromClass(class),值就是一个数组,这个数组里面存放着一个类的所有属性。这样当我们下一次还要对同一个类进行模型赋值操作,就可以直接从这个字典里面取出这个类的一个包含所有属性的数组了。

  2. MJProperty这个类中,通过runtime的动态关联属性的方法,关联每一个objc_property_t,注意是与类对象相关联。value是MJProperty对象:

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    //这里的self是MJProperty类对象
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        //给MJProperty类对象添加关联对象,使用property作为key,关联propertyObj实例对象
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

想象一种情况,Teacher和Student都继承自Person,所以Teacher和Student都有Person的属性,当我们先给Teacher模型赋值的时候,Person类的每一个属性已经调用了上面的代码块封装成了MJProperty对象,并与MJProperty类对象相关联。那么当我们再给Student模型赋值的时候,也会遍历Person类的属性,但是这个时候通过MJProperty *propertyObj = objc_getAssociatedObject(self, property);已经能得到MJProperty对象了,不用去创建。

  1. 在MJPropertyType中有一个types字典,这个字典是在单例中初始化的,types字典的key是code,value是MJPropertyType对象,每次有新的code,就添加到这个字典里面去,这样的好处就是如果code一致,就可以直接从字典中取MJPropertyType。

  2. 每一个MJProperty对象都有一个propertyKeysDict字典,这个字典的key是NSStringFromClass(class),值是一个数组,比如一个MJProperty的名字是name.info[1].text,那么这个数组就会包括4个MJPropertyKey对象,分别表示name,info,1,text,这些key是在取值的时候用的。那么问题来了,为什么要设计字典来存储呢 ,直接用一个数组来存储不就好了吗?
    其实这个问题和2相似,因为我们在第二次遍历Person类中的属性的时候不用去创建一个MJProperty对象,直接通过关联属性去取值就好了,但是Student模型和Teacher模型它们的propertyKeys是有可能不一样的,所以这里需要一个key来加以区分。

笔记地址:MJExtension笔记
原文地址:MJExtension源码解读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345