iOS源码阅读 —— MJExtension

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

1
2
3
4
5
6
7
8
9
10
11
// 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模型;如果不传,返回已赋值的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (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对象,并获取当前类的类型,以及黑白名单属性。

1
2
3
4
5
6
7
// 获得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参数进行回调,在回调的代码块中,对每个属性进行注意赋值。

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。
[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.检测是否被忽略

1
2
3
4
// 白名单
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
// 黑名单
if ([ignoredPropertyNames containsObject:property.name]) return;

判断黑白名单中是否包含相应的属性名称。

1.取出属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.复杂处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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.赋值

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

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

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

1
2
3
4
5
6
7
8
/**
* 设置成员变量的值
*/
- (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的方法主要有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 转换并返回模型中所有属性的键值对
- (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.检测是否被忽略

1
2
3
4
5
6
7
8
// 白名单
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取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 方法,直至最终得到非模型和非数组的数据类型。

1
2
3
4
5
6
7
8
9
10
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方法中返回的自定义映射表。
对于没有引用替换键的值,可以直接赋值。

1
keyValues[property.name] = value;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 获取原始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风险。
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信