A fast, convenient and nonintrusive conversion between JSON and model.
转换速度快、使用简单方便的字典转模型框架
总体来说:上手快,使用简单。
关键类
#import "NSObject+MJCoding.h"
#import "NSObject+MJProperty.h"
#import "NSObject+MJClass.h"
#import "NSObject+MJKeyValue.h"
#import "NSString+MJExtension.h"
#import "MJExtensionConst.h"
除此之外还有一些辅助类,比如:MJFoundation.h
、MJProperty.h
、MJPropertyKey.h
、MJPropertyType.h
。
根据类的名字,大致就能猜到是用来做什么用的。我们先从最简单的开始分析。
MJExtensionConst
这里定义了整个库的常量和宏定义。类似于一个项目中的预编译文件。包含了方法弃用宏
,自定义Log日志
、自定义断言
、快速打印所有属性
、自定义类型编码字符串常量
- 方法弃用:一般在做项目中比较少用到,做
SDK
用到的机会比较大。#define MJExtensionDeprecated(instead) NS_DEPRECATED(2_0, 2_0, 2_0, 2_0, instead)
可能好多同学对这里的2_0不是很懂。这里简单扩展一下:
一、NS_AVAILABEL_IOS
例如:
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^)(void))completion NS_AVAILABLE_IOS(5_0);
该NS_AVAILABLE_IOS(5_0)告诉我们这个方法可以在iOS5.0及以后的版本中使用。如果我们在比指定版本更老的版本中调用这个方法,就会引起崩溃。
二、NS_AVAILABLE
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
这里的NS_AVAILABLE宏告诉我们这方法分别随Mac OS 10.8和iOS 6.0被引入。
三、NS_DEPRECATED_IOS
例如:
- (void)presentModalViewController:(UIViewController *)modalViewController animated:(BOOL)animated NS_DEPRECATED_IOS(2_0, 6_0);
NS_DEPRECATED_IOS(2_0, 6_0)
这里的宏有两个版本号,前面一个表明了这个方法被引入时的iOS版本,后面一个表名它被废弃时的iOS版本。本废弃并不是指这个方法就不存在了,知识意味着我们应当开始考虑将相关代码迁移到新的API上去了。
四、NS_DEPRECATED
例如:
- (void)removeObjectsFromIndices:(NSUInteger *)indices numIndices:(NSUInteger)cnt NS_DEPRECATED(10_0, 10_6, 2_0, 4_0);
这里表示这个方法虽Max OS 10.0和iOS 2.0被引入,在Mac OS 10.6和iOS 4.0后背废弃。
```
根据上面的解释,我们可以知道这里的`#define MJExtensionDeprecated(instead) NS_DEPRECATED(2_0, 2_0, 2_0, 2_0, instead)`代码在Max OS 2.0和iOS 2.0被引入,在Mac OS 2.0和iOS 2.0后背废弃。是不是有什么不对??😒😒
- 宏定义函数(日志输出,构建错误等):看一个断言的例子
```
#define MJExtensionAssertError(condition, returnValue, clazz, msg) \
[clazz setMj_error:nil]; \
if ((condition) == NO) { \
MJExtensionBuildError(clazz, msg); \
return returnValue;\
}
有一个细节`(condition)`,这里为什么要用(condition)多加一对括号,因为conditionh是一个表达式,为了保证执行顺序。
这种特性是从c语言移植过来的,在宏定义每行最后加上斜杠`\`,表示宏定义函数,如果有参数可以按照上面的例子进行传递。
-
最后来看一看类型编码:大致根据名称就知道符号代表什么类型的参数
NSString *const MJPropertyTypeInt = @"i";
NSString *const MJPropertyTypeShort = @"s";
NSString *const MJPropertyTypeFloat = @"f";
NSString *const MJPropertyTypeDouble = @"d";
NSString *const MJPropertyTypeLong = @"l";
NSString *const MJPropertyTypeLongLong = @"q";
NSString *const MJPropertyTypeChar = @"c";
NSString *const MJPropertyTypeBOOL1 = @"c";
NSString *const MJPropertyTypeBOOL2 = @"b";
NSString const MJPropertyTypePointer = @"";
NSString *const MJPropertyTypeIvar = @"^{objc_ivar=}";
NSString *const MJPropertyTypeMethod = @"^{objc_method=}";
NSString *const MJPropertyTypeBlock = @"@?";
NSString *const MJPropertyTypeClass = @"#";
NSString *const MJPropertyTypeSEL = @":";
NSString *const MJPropertyTypeId = @"@";
## MJFoundation.h
这个类做的事情就是判断类是不是属于`Foundation`框架里面的。可能很好奇如何判断这个类是不是`Foundation `框架呢。这里用的是穷举法,由于框架里面的类太多。这里就把最后常用的几个类进行判断。如下:
foundationClasses_ = [NSSet setWithObjects:
[NSURL class],
[NSDate class],
[NSValue class],
[NSData class],
[NSError class],
[NSArray class],
[NSDictionary class],
[NSString class],
[NSAttributedString class], nil];
然后判断
-
(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;
}
这里加`__block`是为了在`block`内部可以修改`result`.
## NSString+MJExtension.h
这个类作用就是对字符串的一些操作,比如变量的命名方法转换。常见的`驼峰转下划线(loveYou -> love_you)`、`下划线转驼峰(love_you -> loveYou)`、`首字母变大写`、`首字母变小写`、`是不是整形`、`是不是URL格式`
**其实就是一个工具分类,操作变量或者属性名**
## MJPropertyType(基础类型)
对属性的具体类型进行封装,简单来说就是讲类型编码转为我们熟知的类型。暴露给外面使用,其属性如下:
/** 类型标识符,类型编码 */
@property (nonatomic, copy) NSString *code;
/** 是否为id类型 */
@property (nonatomic, readonly, getter=isIdType) BOOL idType;
/** 是否为基本数字类型:int、float等 */
@property (nonatomic, readonly, getter=isNumberType) BOOL numberType;
/** 是否为BOOL类型 */
@property (nonatomic, readonly, getter=isBoolType) BOOL boolType;
/** 对象类型(如果是基本数据类型,此值为nil) */
@property (nonatomic, readonly) Class typeClass;
/** 类型是否来自于Foundation框架,比如NSString、NSArray /
@property (nonatomic, readonly, getter = isFromFoundation) BOOL fromFoundation;
/* 类型是否不支持KVC */
@property (nonatomic, readonly, getter = isKVCDisabled) BOOL KVCDisabled;
通过类方法初始化`+ (instancetype)cachedTypeWithCode:(NSString *)code`。
**因为总共的类型就如上提到的那几种,所以这里用了一个静态的字典保存结果。下次直接从结果中取,如果有则不用依次判断了**
static NSMutableDictionary *types_;// 静态字典
- (void)initialize
{
types_ = [NSMutableDictionary dictionary];
}
注意这里的`initialize `,如果你对`Swizzle`熟悉,那么`load`应该知道。顺便就提下两者的区别。
|对比点| +(void)load|+(void)initialize|
|:--:|:--:|:--:|
| 执行时机 |在程序运行后立即执行| 在类的方法第一次被调时执行|
|若自身未定义,是否沿用父类的方法?| 否| 是|
| 类别中的定义 | 全都执行,但后于类中的方法 | 覆盖类中的方法,只执行一个|
那缓存怎么实现的呢?
一般缓存的思路都一样:在第一次使用的时候保存结果。如果以后有相同的情况则直接从以前的结果中返回就可以了。
-
(instancetype)cachedTypeWithCode:(NSString *)code
{
MJExtensionAssertParamNotNil2(code, nil);// 从缓存中取,如果没有才去设置
MJPropertyType *type = types_[code];
if (type == nil) {
type = [[self alloc] init];
type.code = code;
// 存储起来
types_[code] = type;
}
return type;
}
接下里看看类型编码的转换部分。
// 参数类型转换
-
(void)setCode:(NSString *)code
{
_code = code;MJExtensionAssertParamNotNil(code);
if ([code isEqualToString:MJPropertyTypeId]) {
_idType = YES;
} else if (code.length == 0) {
_KVCDisabled = YES;
} else if (code.length > 3 && [code hasPrefix:@"@""]) {
// 去掉@"和",截取中间的类型名称
_code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
// 得到类型类型
_typeClass = NSClassFromString(_code);
_fromFoundation = [MJFoundation isClassFromFoundation:_typeClass];
// 是否为NSNumber类型
_numberType = [_typeClass isSubclassOfClass:[NSNumber class]];} else if ([code isEqualToString:MJPropertyTypeSEL] ||
[code isEqualToString:MJPropertyTypeIvar] ||
[code isEqualToString:MJPropertyTypeMethod]) {
_KVCDisabled = YES;
}// 是否为数字类型
NSString *lowerCode = _code.lowercaseString;
NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeLongLong, MJPropertyTypeChar];// 是否为基本数据类型
if ([numberTypes containsObject:lowerCode]) {
_numberType = YES;if ([lowerCode isEqualToString:MJPropertyTypeBOOL1] || [lowerCode isEqualToString:MJPropertyTypeBOOL2]) { _boolType = YES; }
}
}
比较重要的是这一段
if ([code isEqualToString:MJPropertyTypeId]) {
_idType = YES;
} else if (code.length == 0) {
_KVCDisabled = YES;
} else if (code.length > 3 && [code hasPrefix:@"@""]) {
// 去掉@"和",截取中间的类型名称
_code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
// 得到类型类型
_typeClass = NSClassFromString(_code);
_fromFoundation = [MJFoundation isClassFromFoundation:_typeClass];
// 是否为NSNumber类型
_numberType = [_typeClass isSubclassOfClass:[NSNumber class]];
} else if ([code isEqualToString:MJPropertyTypeSEL] ||
[code isEqualToString:MJPropertyTypeIvar] ||
[code isEqualToString:MJPropertyTypeMethod]) {
_KVCDisabled = YES;
}
传进来的`code`就是属性所对应的属性名称,比如`NSArray`对应`@"NSArray"`。自定义的类,签名都会有一个`@`符号,后面接上类名。形式如`@Classname`。
## MJPropertyKey.h
这个类是对属性进行分类。所有属性可以归为两类一种是字典,也就是键值对(`NSDictionary`)和数组(`NSArray`)。
外面通过调用`- (id)valueInObject:(id)object`从传进来的`id`类型中取值。
## MJProperty.h
对`objc_property_t`封装,存储一个属性值相关的详细信息,将`objc_property_t`转为能够直接用的对象。它是对`objc_property_t`类型的一次封装,便于我们使用。同事它也依赖于上面所介绍的几种数据类型。从`.h`文件看到`#import "MJPropertyType.h" #import "MJPropertyKey.h"`。同样也依赖于`#import "MJFoundation.h" #import "MJExtensionConst.h"`
初始化入口函数`+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property`。正如上面所说目的就是将`runtime`中的`objc_property_t `转为`MJProperty`方便我们的使用。
具体实现:
- (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
MJProperty *propertyObj = objc_getAssociatedObject(self, property);
if (propertyObj == nil) {
propertyObj = [[self alloc] init];
// 转为MJProperty
propertyObj.property = property;
objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return propertyObj;
}
` MJProperty *propertyObj = objc_getAssociatedObject(self, property);`这句话是动态给类添加属性,也就是说,给类添加了一个属性名为`property `的`MJProperty`类型属性。**注意这里是一个类方法,也就是这里的`self`代表什么。**
这里的缓存方法是通过懒加载的形式实现的,将需要缓存的属性动态添加到类上面。如果没有则新加属性有则直接返回属性。这样做的目的是为了一个类可能在项目中会用到很多次字典转模型。所以保存一份之后就不用每次都创建新的属性标识了。**以空间换时间**
把这个方法执行完,就得到了如下三个属性的值:
/** 成员属性 /
@property (nonatomic, assign) objc_property_t property;
/* 成员属性的名字 */
@property (nonatomic, readonly) NSString *name;
/** 成员属性的类型 */
@property (nonatomic, readonly) MJPropertyType *type;
除了公开的属性还有两个私有属性:
@property (strong, nonatomic) NSMutableDictionary *propertyKeysDict;
@property (strong, nonatomic) NSMutableDictionary *objectClassInArrayDict;
他们分别保存了的类型是`MJPropertyKey`
通过
/**
- 设置object的成员变量值
*/
- (void)setValue:(id)value forObject:(id)object;
/**
- 得到object的成员属性值
*/
- (id)valueForObject:(id)object;
对特定的属性存取值。
/** 非数组类型 */
- (void)setOriginKey:(id)originKey forClass:(Class)c;
- (NSArray *)propertyKeysForClass:(Class)c;
/** 模型数组中的保存模型类型 */
- (void)setObjectClassInArray:(Class)objectClass forClass:(Class)c;
- (Class)objectClassInArrayForClass:(Class)c;
> 记:本来该上个周就完成的。拖到了现在还没有分析完。
## NSObject+MJClass.h
从这个类开始,就进入了分析`NSObject`分类的程度了。前面分析的对象是为`NSObject`所依赖的。
这个类的功能大致可以归为`遍历`、`属性白名单`、`属性黑名单`。所以可以重点来看看这三个部分。
外部通过`+ (NSMutableArray *)mj_totalIgnoredPropertyNames;`和`+ (NSMutableArray *)mj_totalAllowedCodingPropertyNames;`获得黑名单与白名单。
### 类的遍历
遍历就两个方法:
- (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration;
- (void)mj_enumerateAllClasses:(MJClassesEnumeration)enumeration;
`mj_enumerateClasses `只要当前遍历的是`Foundatoin`框架的就会退出遍历,否则会一直沿着继承树遍历。`mj_enumerateAllClasses `会遍历继承树上所有的类。**为什么会存在遍历到`Foundatoin`框架就停止遍历了,因为我们自定义的模型大部分是继承至NSObject这中类型。这是为什么停止,那为什么要遍历。因为自定义的模型可能继承自己我们自定义的模型。为了保护所有的信息,比如属性信息,所以需要遍历。**
注意这里的参数是其实是一个`typedef void (^MJClassesEnumeration)(Class c, BOOL *stop);`Block。写法有点类似系统中数组遍历。这种写法值得学习,平时我们遍历都是在类中直接调用一个方法,而通过这样传递`Block`这样就更加解耦了。其实也可以通过`Target-Action`模式实现。注意这里的`Bool`类型传的是指针哦,就像`*stop`。
相关的实现:
-
(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;
}
}
重点看看这句
// 4.1.执行操作
// 对一般类型取地址传递
enumeration(c, &stop);
### 白名单配置
配置白名单是为了对属性进行过滤。只有在白名单中的属性名才会进行字典和模型的转换。来说一下这里涉及的配置方法。
外部调用者,通过类方法传入`Block`参数进行配置。`typedef NSArray * (^MJAllowedPropertyNames)();`这是传入的`Block`定义。可以看到返回的是一个数组。为什么不直接将白名单属性暴露处理出来给调用者直接使用呢?大概是遵循了设计模式中的`知道最少原则`。
在`.m`文件中定义了几个保存白名单、黑名单的静态数组。定义如下:
static NSMutableDictionary *allowedPropertyNamesDict_;
static NSMutableDictionary *ignoredPropertyNamesDict_;
static NSMutableDictionary *allowedCodingPropertyNamesDict_;
static NSMutableDictionary *ignoredCodingPropertyNamesDict_;
为了保存传入的Block信息,需要给分类动态添加属性。
// 在分类中新增属性
static const char MJAllowedPropertyNamesKey = '\0'; // 白名单
static const char MJIgnoredPropertyNamesKey = '\0'; // 黑名单
static const char MJAllowedCodingPropertyNamesKey = '\0'; // 归档白名单
static const char MJIgnoredCodingPropertyNamesKey = '\0'; // 归档黑名单
比如这里的`MJAllowedPropertyNamesKey `就是白名单传进来的属性名称了。
设置白名单的入口:
- (void)mj_setupAllowedPropertyNames:(MJAllowedPropertyNames)allowedPropertyNames;
{
[self mj_setupBlockReturnValue:allowedPropertyNames key:&MJAllowedPropertyNamesKey];
}
最终调用的是`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];
}
可以很清楚的看到这里将block作为自身的属性。
**除了这种方式还有一种:**那就是在类中实现`mj_allowedPropertyNames`比如:
- (NSArray *)mj_allowedPropertyNames {
return @[@"name",@"icon"];
}
后面获取可用属性的时候会对这两种方式都判断。
### 设置黑名单
设置黑名单的方式和设置白名单类似。
### 最终可转换的数组
调用这个方法`mj_totalIgnoredPropertyNames `就是返回经过过滤后的属性。
一共有两种方式,一种是通过`Selecotr`一种是通过`Block`设置。
-
(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;
}
#### 通过Selector
`selector`是使用者如果想添加白名单需要自定义实现类方法`mj_allowedPropertyNames`。返回的是一个白名单数组。黑名单原理类似。`key`是指定过滤的是白名单还是黑名单。
`selector`的方式需要给对应的类添加一个类方法如:
- (NSArray *)mj_allowedPropertyNames {
return @[@"name",@"icon"];
}
#### 通过Block
> 疑惑:前面设置的是一个返回值类型为数组的`Block`。代码:
> ```
if (block) {
objc_setAssociatedObject(self, key, block(), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} else {
objc_setAssociatedObject(self, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
但是取得时候又是直接取得数组。难道这个时候block
会自动执行,然后返回执行结果
[self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
NSArray *subArray = objc_getAssociatedObject(c, key);
[array addObjectsFromArray:subArray];
}];
**最后通过实践证明,确实能够从`objc_getAssociatedObject(c, key);`得到对应的数组。也就是作为属性的`block`执行了。**
## NSObject+MJCoding.h
这个类是用于归档。只有实现了`MJCoding`协议的类才能够归档。
/**
- 这个数组中的属性名才会进行归档
*/
- (NSArray )mj_allowedCodingPropertyNames;
/*
- 这个数组中的属性名将会被忽略:不进行归档
*/
- (NSArray *)mj_ignoredCodingPropertyNames;
在进行归档的时候,我们只需要在`Implemenion`中添加`MJExtensionCodingImplementation`。
实际上是宏定义了`NSCode`进行归档,解档。
define MJCodingImplementation \
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
[self mj_decode:decoder];
}
return self;
}
\ - (void)encodeWithCoder:(NSCoder *)encoder
{
[self mj_encode:encoder];
}
看一看归档的部分,解档的步骤一样。
-
(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];
}];
}
直接看重点:
id value = [property valueForObject:self];
if (value == nil) return;
[encoder encodeObject:value forKey:property.name];
## NSObject+MJProperty.h
这个类保存的属性的一些配置。大致可分为:
- 1.遍历属性
- 2.新值配置
- 3.key配置
- 4.array model class配置
### 遍历属性
`+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration`遍历所有属性的入口。这个方法在很多地方都存在过。
属性会从缓存中取,`NSArray *cachedProperties = [self properties];`。`[self properties]`是缓存属性的部分。然后就直接遍历所有的属性。
-
(void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration
{
// 获得成员变量
// 从类的继承树遍历每一个类,从类中获得属性
NSArray *cachedProperties = [self properties];// 遍历成员变量
BOOL stop = NO;
for (MJProperty *property in cachedProperties) {
enumeration(property, &stop);
if (stop) break;
}
}
这里遍历用了`block`,外部传递`block`进来遍历。`typedef void (^MJPropertiesEnumeration)(MJProperty *property, BOOL *stop);`这个库很多地方都用到了类似的遍历方式。
### 新值配置
新值配置是什么意思?:就是改变特定的属性的原有值,这样更加灵活。
同样有两种方式`Block`和`类方法`。
存取方法如下:
- (void)mj_setupNewValueFromOldValue:(MJNewValueFromOldValue)newValueFormOldValue;
- (id)mj_getNewValueFromObject:(__unsafe_unretained id)object oldValue:(__unsafe_unretained id)oldValue property:(__unsafe_unretained MJProperty *)property;
这里的`typedef id (^MJNewValueFromOldValue)(id object, id oldValue, MJProperty *property);`其实和下面方法参数形式是一样的。
看了一下这个方法,一次只支持一个属性新值配置。
- (void)mj_setupNewValueFromOldValue:(MJNewValueFromOldValue)newValueFormOldValue
{
objc_setAssociatedObject(self, &MJNewValueFromOldValueKey, newValueFormOldValue, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
如何获取新?肯定需要兼容两种设置方式,一种`Block`,一种通过方法设置。
-
(id)mj_getNewValueFromObject:(__unsafe_unretained id)object oldValue:(__unsafe_unretained id)oldValue property:(MJProperty *__unsafe_unretained)property{
// 如果有实现方法
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;
}
思路和上面分析白名单,黑名单设置的方式一样。
### key配置
`key`配置是解决属性名成需要重新定义的情况。
这个配置统一通过`Block`设置。
这里注意到为什么在动态添加了属性之后需要将`cachedPropertiesDict_`字典里面的清空一次。如下:
-
(void)mj_setupReplacedKeyFromPropertyName:(MJReplacedKeyFromPropertyName)replacedKeyFromPropertyName
{
[self mj_setupBlockReturnValue:replacedKeyFromPropertyName key:&MJReplacedKeyFromPropertyNameKey];[[self dictForKey:&MJCachedPropertiesKey] removeAllObjects];
} -
(void)mj_setupReplacedKeyFromPropertyName121:(MJReplacedKeyFromPropertyName121)replacedKeyFromPropertyName121
{
objc_setAssociatedObject(self, &MJReplacedKeyFromPropertyName121Key, replacedKeyFromPropertyName121, OBJC_ASSOCIATION_COPY_NONATOMIC);[[self dictForKey:&MJCachedPropertiesKey] removeAllObjects];
}
这样做的目的是为了保证缓存数组中的数据是最新的。因为我们替换了属性的key,所以要用最新的。在获取所有属性中。有这么一段:
NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
if (cachedProperties == nil) {
cachedProperties = [NSMutableArray array];
// 遍历类继承树,一直遍历到fondation框架。也就是到NSObject就停止遍历。因为我们模型都是从NSObject开始继承的
[self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
// 1.获得所有的成员变量
......
}
可以看到只有属性为空才会遍历类,获取最新属性。
### array model class配置
这个方法是处理模型中包含一另一个模型数组。**在实际运用比较多。**
-
(void)mj_setupObjectClassInArray:(MJObjectClassInArray)objectClassInArray
{
[self mj_setupBlockReturnValue:objectClassInArray key:&MJObjectClassInArrayKey];[[self dictForKey:&MJCachedPropertiesKey] removeAllObjects];
}
和上面的方式一样,会将最终会作为类的一个属性将模型数组字典保存下来。方便后面使用。
static const char MJReplacedKeyFromPropertyNameKey = '\0';
static const char MJReplacedKeyFromPropertyName121Key = '\0';
static const char MJNewValueFromOldValueKey = '\0';
static const char MJObjectClassInArrayKey = '\0';
static const char MJCachedPropertiesKey = '\0';
这是`NSObject+MJProperty.h`动态添加的所有属性。通过名字可以知道它的用途。
## NSObject+MJKeyValue.h
**这个类的内容有点多**
这个类中有一个很重要的协议`MJKeyValue`。
- (NSArray *)mj_allowedPropertyNames;
- (NSArray *)mj_ignoredPropertyNames;
- (NSDictionary *)mj_replacedKeyFromPropertyName;
- (id)mj_replacedKeyFromPropertyName121:(NSString *)propertyName;
- (NSDictionary *)mj_objectClassInArray;
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property;
- (void)mj_keyValuesDidFinishConvertingToObject;
- (void)mj_objectDidFinishConvertingToKeyValues;
这里的协议规定了白名单、黑名单。之前分析过通过`block`的形式同样能够实现黑、白名单。我猜测这个文件是很久之前就有了为了兼容性,所以这种设置白、黑名单的方式一致保留着。
**这也是我们分析的最后一个类,可能也是最复杂的一个类**
分别是:
- 类方法
- 错误定义
- 转换字典`replace`设置
- 模型转字典
- 转所有属性
- 制定部分属性转
- 忽略部分属性转
- 模型数组转字典数组
- 字典转模型
- 字典转为模型(可以是NSDictionary、NSData、NSString)
- 字典转为模型(可以是NSDictionary、NSData、NSString)**CoreData支持**
- 字典数组转模型数组
- 字典数组来创建一个模型数组(可以是NSDictionary、NSData、NSString)
- 字典数组来创建一个模型数组(可以是NSDictionary、NSData、NSString)**CoreData支持**
- plist来创建一个模型数组
- 仅限于mainBundle中的文件
- 文件全路径
- 模型转json、字典、数组
- 转换为JSON Data
- 转换为字典或者数组
- 转换为JSON 字符串
- 对象方法
- 字典转模型
- 字典转为模型(可以是NSDictionary、NSData、NSString)
- 字典转为模型(可以是NSDictionary、NSData、NSString)**CoreData支持**
上面所列的内容就是这个框架提供给使用者的所有功能了。这个部分就是把之前分析的内容串在一起。实现这些功能。
我们从最常用的`+ (instancetype)mj_objectWithKeyValues:(id)keyValues;`方法入手。从头到尾走一遍这个流程。
直接看最核心部分。入口就是`- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context`这个方法。我们把外部的字典,json传入,最终把字典的`key`映射到对应的属性上,`value`成为这个属性的值。
### 参数过滤
第一步肯定是对参数合法性进行校验。
keyValues = [keyValues mj_JSONObject];
MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典");
经过转换后还不是字典类型就直接抛出异常。
### 黑名单,白名单过滤
接下来如果使用者配置了属性的白名单或者黑名单,则会对取出黑白名单。在遍历类的属性的时候过滤掉。
NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
因为属性列表是在类对象上,所以自然去`NSObject+MJClass.h`调用。这个类主要功能就是提供了黑白名单的存储。
`NSObject+MJClass.h`中存储的黑白名单
static const char MJAllowedPropertyNamesKey = '\0'; // 白名单
static const char MJIgnoredPropertyNamesKey = '\0'; // 黑名单
static const char MJAllowedCodingPropertyNamesKey = '\0'; // 归档白名单
static const char MJIgnoredCodingPropertyNamesKey = '\0'; // 归档黑名单
### 遍历类的所有属性
接下来就是对类的每个属性处理,比如替换,忽略等。涉及到属性的是在`NSObject+MJProperty.h`类中完成的。比如遍历就是。
通过传入`block`,在遍历的同时对属性就行处理。形式就像通过`enumerateObjectsUsingBlock:`遍历。
[someArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}]
遍历属性
[clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
......
}];
如果属性在白名单或者黑名单中出现在则直接跳出这次循环。
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
if ([ignoredPropertyNames containsObject:property.name]) return;
取出属性对应的值,当然这里增加了对值得过滤,比如设置了新值替换旧的值。如果最终取出的结果中没有值,则直接返回。
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;
接下里就是最终处理部分了。这时候属性的`key`和`value`都是合法的了。
- 1.剩下的就是处理属性。比如讲不可变数组转为可变数组。
- 2.如果不是`Foundation `框架的类,也就是继承至自定义模型的需要递归遍历。
- 3.对模型数组的处理
- 4.如果是`Foundation `框架中的。这个部分就可以直接给`value`赋值了。
- 5.最终`KVC`给属性赋值。`[property setValue:value forObject:self];`
网友评论
2、MJProperty *propertyObj = objc_getAssociatedObject(self, property);这句话是动态给类添加属性,也就是说,给类添加了一个属性名为property的MJProperty类型属性,这个地方完全错误,应该是根据property从self类中取值,恳请作者更改指正!