美文网首页程序员
iOS源码阅读 —— MJExtension

iOS源码阅读 —— MJExtension

作者: GG266 | 来源:发表于2020-09-11 09:01 被阅读0次

MJExtension是一款开源的,简单易用的字典与模型转换框架。
常用的方法,主要是以下几个:

// JSON|字典 转 模型
+ (instancetype)mj_objectWithKeyValues:(id)keyValues;

// 通过 JSON|字典 为 模型赋值
- (instancetype)mj_setKeyValues:(id)keyValues;

// 模型转JSON
- (NSMutableDictionary *)mj_keyValues;

// JSON数组转模型数组
+ (NSMutableArray *)mj_objectArrayWithKeyValuesArray:(id)keyValuesArray;

功能

JSON|字典 转 模型

+ mj_objectWithKeyValues:

+ mj_objectWithKeyValues: 是框架中最简单的JSON转模型的方法,通过直接调用类方法并传入JSON数据即可快速实现转换。而在+ mj_objectWithKeyValues:方法中,实际是调用了+ mj_objectWithKeyValues: context:方法,参数中如果传了contenxt,最终会返回CoreData模型;如果不传,返回已赋值的模型。

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

在为实例赋值的方法中,由于- mj_setKeyValues:实际的实现是调用- mj_setKeyValues: context:。所以我们直接进入- mj_setKeyValues: context:进行分析。

首先,需要将传入的keyValues处理成可用的JSON对象,并获取当前类的类型,以及黑白名单属性。

// 获得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];

紧接着,调用类的扩展方法+ mj_enumerateProperties:,获取和遍历类的属性列表,通过block参数进行回调,在回调的代码块中,对每个属性进行注意赋值。

核心代码:

    //通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。
    [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) { // 模型属性
                // 既不是基础类型,也不是NS类型。即:基本数据类型
                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 -> NSDecimalNumber, 使用 DecimalNumber 来转换数字, 避免丢失精度以及溢出
                    NSDecimalNumber *decimalValue = [NSDecimalNumber decimalNumberWithString:oldValue
                                                                                      locale:numberLocale];
                    
                    // 检查特殊情况
                    if (decimalValue == NSDecimalNumber.notANumber) {
                        value = @(0);
                    }else if (propertyClass != [NSDecimalNumber class]) {
                        value = [decimalValue mj_standardValueWithTypeCode:type.code];
                    } else {
                        value = decimalValue;
                    }
                    
                    // 如果是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.赋值(KVC)
            [property setValue:value forObject:self];
        } @catch (NSException *exception) {
            MJExtensionBuildError([self class], exception.reason);
            MJExtensionLog(@"%@", exception);
        }
    }];

从代码中,可以直观看出,赋值操作主要分为步骤4个步骤。

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;

因为同一个成员属性,父类和子类的行为可能不一致(originKey、propertyKeys、objectClassInArray),所以其键值可能是一个数组,通过循环这个数组尝试获取值。
对值得过滤,指的是使用者通过实现- (id)mj_newValueFromOldValue: property:方法,对结果进行进一步的处理(比如字符串日期处理为NSDate、字符串nil处理为@"")。

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) { // 模型属性
    // 既不是基础类型,也不是NS类型。即:基本数据类型
    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]) {


} else if ([value isKindOfClass:[NSString class]]) {

} else if ([value isKindOfClass:[NSNumber class]] && propertyClass == [NSDecimalNumber class]){

}

复杂处理,主要是对属性值的类型进行判断,属性值的类型只要分为:模型属性(自定义类)、数组属性和其他属性(NS类型)。
模型属性的value,需要通过继续调用- mj_objectWithKeyValues:value context:方法,将字典转换成模型。
数组属性的值,则需要根据数组中模型的类型,进行循环转换。
其他情况的值,可以通过简单的转化或者直接使用。

3.赋值

至此,属性信息和值都有了。

// 经过转换后, 最终检查 value 与 property 是否匹配
if (propertyClass && ![value isKindOfClass:propertyClass]) {
    value = nil;
}
[property setValue:value forObject:self];

在确定 value的值 与 property得类型 确实匹配后,通过KVC进行赋值。

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

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

模型 转 JSON|字典

- mj_keyValues

模型转JSON的方法主要有:

// 转换并返回模型中所有属性的键值对
- (NSMutableDictionary *)mj_keyValues;

/**
 @para keys 需要返回的特定键的数组 
 @return 特定关键词的键值对
 */
- (NSMutableDictionary *)mj_keyValuesWithKeys:(NSArray *)keys;

/**
 @para ignoredKeys 需要忽略的特定键的数组 
 @return 除特定关键词的其他有效键值对
 */
- (NSMutableDictionary *)mj_keyValuesWithIgnoredKeys:(NSArray *)ignoredKeys;

以上方法统一调用了- mj_keyValuesWithKeys:ignoredKeys:,让我们直接进入这个方法一探究竟。
- mj_keyValuesWithKeys:ignoredKeys:方法与JSON转模型的核心逻辑是极其相似的,即通过遍历类的所有属性,进行相关操作,这里我们直接进入代码块,进行分析。

0.检测是否被忽略

// 白名单
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
// 黑名单
if ([ignoredPropertyNames containsObject:property.name]) return;
// 只需要返回的特定键
if (keys.count && ![keys containsObject:property.name]) return;
// 需要被忽略的特定键
if ([ignoredKeys containsObject:property.name]) return;

返回的结果,不仅可以对黑白名单中的属性进行筛选,还可以根据具体场景设置需要返回和忽略的特定键值。

1.取出属性值

使用KVC取值。

id value = [property valueForObject:self];

/**
 *  获得成员变量的值
 */
- (id)valueForObject:(id)object
{
    if (self.type.KVCDisabled) return [NSNull null];
    
    id value = [object valueForKey:self.name];
    
    // 32位BOOL类型转换json后成Int类型
    /** https://github.com/CoderMJLee/MJExtension/issues/545 */
    // 32 bit device OR 32 bit Simulator
#if defined(__arm__) || (TARGET_OS_SIMULATOR && !__LP64__)
    if (self.type.isBoolType) {
        value = @([(NSNumber *)value boolValue]);
    }
#endif
    
    return value;
}

2.模型属性和数组的处理

如果当前的属性属于模型类型或数组,则需要对 value 进行递归调用 - mj_keyValues 方法,直至最终得到非模型和非数组的数据类型。

MJPropertyType *type = property.type;
Class propertyClass = type.typeClass;
if (!type.isFromFoundation && propertyClass) {
    value = [value mj_keyValues];
} else if ([value isKindOfClass:[NSArray class]]) {
    // 3.处理数组里面有模型的情况
    value = [NSObject mj_keyValuesArrayWithObjectArray:value];
} else if (propertyClass == [NSURL class]) {
    value = [value absoluteString];
}

3.赋值

在对结果keyValues进行赋值之前,需要先判断创建键值时,是否引用了替换键 —— 也就是在+ mj_replacedKeyFromPropertyName方法中返回的自定义映射表。
对于没有引用替换键的值,可以直接赋值。

keyValues[property.name] = value;

对于引用了替换键的值,需要获取原始的key,最终结果也将返回最原始的JSON或字典。

// 获取原始key
NSArray *propertyKeys = [[property propertyKeysForClass:clazz] firstObject];
NSUInteger keyCount = propertyKeys.count;
// 创建字典
__block id innerContainer = keyValues;
[propertyKeys enumerateObjectsUsingBlock:^(MJPropertyKey *propertyKey, NSUInteger idx, BOOL *stop) {
    // 下一个属性
    MJPropertyKey *nextPropertyKey = nil;
    if (idx != keyCount - 1) {
        nextPropertyKey = propertyKeys[idx + 1];
    }

    if (nextPropertyKey) { // 不是最后一个key
        // 当前propertyKey对应的字典或者数组
        id tempInnerContainer = [propertyKey valueInObject:innerContainer];
        if (tempInnerContainer == nil || [tempInnerContainer isKindOfClass:[NSNull class]]) {
            if (nextPropertyKey.type == MJPropertyKeyTypeDictionary) {
                tempInnerContainer = [NSMutableDictionary dictionary];
            } else {
                tempInnerContainer = [NSMutableArray array];
            }
            if (propertyKey.type == MJPropertyKeyTypeDictionary) {
                innerContainer[propertyKey.name] = tempInnerContainer;
            } else {
                innerContainer[propertyKey.name.intValue] = tempInnerContainer;
            }
        }

        if ([tempInnerContainer isKindOfClass:[NSMutableArray class]]) {
            NSMutableArray *tempInnerContainerArray = tempInnerContainer;
            int index = nextPropertyKey.name.intValue;
            while (tempInnerContainerArray.count < index + 1) {
                [tempInnerContainerArray addObject:[NSNull null]];
            }
        }

        innerContainer = tempInnerContainer;
    } else { // 最后一个key
        if (propertyKey.type == MJPropertyKeyTypeDictionary) {
            innerContainer[propertyKey.name] = value;
        } else {
            innerContainer[propertyKey.name.intValue] = value;
        }
    }
}];

总结

核心代码:

  • JSON|字典转模型的各类方法,最终都会调用- mj_setKeyValues:(id)keyValues context:
  • 模型转JSON|字典的各类方法,最终都会调用- (NSMutableDictionary *)mj_keyValuesWithKeys: ignoredKeys:

性能方面:

  • 使用runtime动态生成类的属性信息,并通过缓存机制进行性能提优。

容错方面

  • 在JSON|字典转模型最后赋值之前,会对值和属性的类型进行一致性的判断。如果不匹配,value会被置为nil,避免潜在的Crash风险。

相关文章

网友评论

    本文标题:iOS源码阅读 —— MJExtension

    本文链接:https://www.haomeiwen.com/subject/bspmektx.html