介绍:
MJExtension
是一个转换速度快、使用简单方便的字典转模型框架, 由国内李明杰
大神所著, 并且开源, 目前国内很多 iOS 项目都有使用该框架.
github地址:https://github.com/CoderMJLee/MJExtension
功能:
MJExtension
是一套字典和模型之间互相转换的超轻量级框架,提供了以下功能:
-
JSON
-->Model
、Core Data Model
-
JSONString
-->Model
、Core Data Model
-
Model
、Core Data Model
-->JSON
-
JSON Array
-->Model Array
、Core Data Model Array
-
JSONString
-->Model Array
、Core Data Model Array
-
Model Array
、Core Data Model Array
-->JSON Array
- 只需要一行代码,就能实现模型的所有属性进行Coding(归档和解档)
安装:
使用CocoaPods
pod 'MJExtension'
Examples【示例】:
// 字典转模型
@interface User : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *icon;
@property (assign, nonatomic) unsigned int age;
@property (copy, nonatomic) NSString *height;
@property (strong, nonatomic) NSNumber *money;
@property (assign, nonatomic) BOOL sex;
@end
/***********************************************/
NSDictionary *dict = @{
@"name" : @"Jack",
@"icon" : @"lufy.png",
@"age" : @20,
@"height" : @"1.55",
@"money" : @100.9,
@"sex" : @1,
};
// JSON -> User
User *user = [User mj_objectWithKeyValues:dict];
源码分析:
MJExtension
一个丰富的字典转模型库,提供了字典/JSON转模型
,JSON转CoreData模型
, 以及模型数据转字典,转JSON,转数组
,以及模型数据的归档与解档
操作.
本文主要分析 字典转模型
的具体流程以及源码:
1. 入口方法:
+ (instancetype)mj_objectWithKeyValues:(id)keyValues:
作为最基本的入口,在调用了该方法之后, 内部会调用 + (instancetype)mj_objectWithKeyValues:(id)keyValues context:(NSManagedObjectContext *)context
方法, 如果传了 context
,则字典会转为 CoreData模型
,如果不传入 context
,则会进行普通的字典转模型
操作,源码如下:
/**
* 通过字典来创建一个模型
* @param keyValues 字典(可以是NSDictionary、NSData、NSString)
* @return 新建的对象
*/
+ (instancetype)mj_objectWithKeyValues:(id)keyValues{
return [self mj_objectWithKeyValues:keyValues context:nil];
}
+ (instancetype)mj_objectWithKeyValues:(id)keyValues context:(NSManagedObjectContext *)context{
// 获得JSON对象(将 JSON字符串或 NSData 转为 字典)
keyValues = [keyValues mj_JSONObject];
// 断言,如果不是字典类型,则直接返回 nil
MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], nil, [self class], @"keyValues参数不是一个字典");
// 判断是否为需要转为 CoreData 模型
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_JSONObject
方法主要用于将JSON字符串或NSData数据转为字典,MJExtensionAssertError
断言则用于判断是否为字典对象,如果不是一个字典,则直接返回 nil, 接下来判断是否转CoreData 模型
, 如果不是转CoreData 模型
,则进行 字典转模型
操作;
1.1 转JSON对象
每次字典转模型前, 都会通过mj_JSONObject
方法,将JSON字符串或者NSData 数据转为字典.也可以过滤掉一些非JSON的错误数据, mj_JSONObject
源码如下:
// 将NSString字符串或者NSData转为字典对象
- (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;
}
1.2 错误处理断言:
MJExtensionAssertError
如果数据不是字典类型或者是其他类型的,断言则会进行处理,直接返回 nil
, MJExtensionAssertError
定义如下:
/**
* 断言-如果条件不为真,则直接返回 nil
* @param condition 条件
* @param returnValue 返回值
*/
#define MJExtensionAssertError(condition, returnValue, clazz, msg) \
[clazz setMj_error:nil]; \
if ((condition) == NO) { \
MJExtensionBuildError(clazz, msg); \
return returnValue;\
}
2. 字典转模型
通过mj_setKeyValues:
方法可以进行字典转模型操作, 他会调用 mj_setKeyValues: context:
方法, 在 mj_setKeyValues: context:
方法内部,主要会进行以下操作
- 白名单以及黑名单属性过滤数据
- 如果设置有
白名单
,则只有白名单数组中的属性名才允许进行字典和模型的转换, -
黑名单
则相反,黑名单数组中的属性名将会被忽略, 不进行字典和模型的转换
- 如果设置有
- 获取当前对象所有的属性信息,包括父类的
- 通过
mj_enumerateProperties
方法,来获取对象所有属性信息,其内部还会多次调用其他方法,用于查询,基本都是通过 block 回调方式进行传值的. - 通过
+ (NSMutableArray *)mj_properties
方法用于获取一个类的所有属性信息 - 通过
mj_enumerateClasses
来遍历对象所属类以及其父类
- 通过
- 取出每个属性的值(对应字典的 value值)
- 对属性的值进行复杂的操作
- 判断属性是否为可变或者不可变,生成可变或者不可变的值
- 如果属性是一个自定义对象,则会继续调用
mj_objectWithKeyValues
进行字典转模型操作 - 如果属性是一个数组模型,则会调用
mj_objectArrayWithKeyValuesArray
进行字典数组转模型数组 - 其他类型处理(NSURL, NSNumber, Bool)等类型的特殊处理...
- 通过
KVC
进行属性赋值操作.
2.1 核心代码
字典转模型的核心代码mj_setKeyValues:
如下:
/**
字典 -> 模型
*/
- (instancetype)mj_setKeyValues:(id)keyValues{
return [self mj_setKeyValues:keyValues context:nil];
}
/**
核心代码:
*/
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context{
// 获得JSON对象---(将 JSON字符串或 NSData 转为 字典)
keyValues = [keyValues mj_JSONObject];
// 断言,如果不是字典类型,则直接返回 nil
MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");
// 或者当前对象的类对象
Class clazz = [self class];
// 只有这个数组中的属性名才允许进行字典和模型的转换
NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
// 这个数组中的属性名将会被忽略:不进行字典和模型的转换
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
// 通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。(此 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) { // 模型属性
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;
}
}
}
} 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];
}
....
return self;
}
2.2 遍历所有的成员进行回调
mj_enumerateProperties方法
用于查询对象所有的成员变量.然后通过enumeration
进行逐一回调操作,在回调函数中进行赋值操作, 源码如下:
// 遍历所有的成员
+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration{
// 创建信号量
MJExtensionSemaphoreCreate
// 通过信号量进行加锁
MJExtensionSemaphoreWait
// 获得该对象成员变量属性列表
NSArray *cachedProperties = [self mj_properties];
// 信号量进行解锁
MJExtensionSemaphoreSignal
// 遍历成员变量
BOOL stop = NO;
for (MJProperty *property in cachedProperties) {
// 回调给上一级函数进行操作
enumeration(property, &stop);
if (stop) break;
}
}
2.3 获取对象属性列表信息(包括父类的成员变量)
mj_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;
// 通过runtime 的 class_copyPropertyList获取所有成员变量,objc_property_t 是一个结构体,是成员变量的底层结构
objc_property_t *properties = class_copyPropertyList(c, &outCount);
// 2.遍历每一个成员变量
for (unsigned int i = 0; i<outCount; i++) {
// 将 objc_property_t类型 包装成一个 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;
/**** 同一个成员属性 - 父类和子类的行为可能不一致(originKey、propertyKeys、objectClassInArray) ****/
/** 设置最原始的key */
[property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
[property setObjectClassInArray:[self mj_propertyObjectClassInArray:property.name] forClass:self];
[cachedProperties addObject:property];
}
// 3.释放内存
free(properties);
}];
[self mj_propertyDictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
}
}
return cachedProperties;
}
2.4 遍历所有的类(直到NSObject 为止)
mj_enumerateClasses
方法会遍历当前类所属所有类, 从当前类开始,一级一级往上遍历, 直到遇到基类NSObject
为止,代码如下:
// 遍历所有的类(当前类以及父类)
+ (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;
}
}
3. 总结
MJExtension 字典转模型的大概流程总结:
通过 rumtime
获取对象的所有成员变量信息, 通过KVC
进行赋值操作, 达到了将字典数据映射到了模型对象上;
不过 MJExtension
内部做也了大量容错以及优化操作, 同时还提供了对象转字典,对象转JSON,以及归档等等操作.使得我们开发中,通过一句简单代码就可以做到复杂的操作;