1. NSObject+MJClass
为基类添加了一个 Class
相关的分类,用于获取设置所有关于 Class
的配置。
1.1 核心方法 - 遍历类的继承树
/**
* 遍历所有的类
*/
+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration;
+ (void)mj_enumerateAllClasses:(MJClassesEnumeration)enumeration;
这两个方法都会遍历类的继承树,区别在于:
mj_enumerateAllClasses
会遍历继承树中所有的类,直到NSObject
或者NSManagedObject
为止。mj_enumerateClasses
如果其父类为Foundation
中类的子类,只遍历到当前类
1.2 白名单和黑名单
这个当前类的属性设置到白名单或者黑名单字典中
在 NSObject+MJKeyValue
核心类中,进行模型和字典转换时会用到
白名单设置时不能返回长度为0的数组
如果白名单和黑名单都有设置,那么就会取并集
另外还有一对 归档白名单和黑名单,基本上是类似的,用于归解档
1.2.1 实现原理
设置黑白名单有2中方式:
- 直接调用
NSObject+MJClass
对应的方法, 这种方式会将数组利用Runtime
关联到类对象上
[CZAnimal mj_setupIgnoredPropertyNames:^NSArray *{
return @[@"type"];
}];
- 利用
NSObject+MJKeyValue
提供的方法,这个方式是在运行时获取需要设置的数组
+ (NSArray *)mj_ignoredPropertyNames {
return @[@"type"];
}
不管是哪种方式,在获取过一次后,就会被缓存起来,内部创建了4个静态局部字典变量用来缓存白名单和黑名单的数据
- allowedPropertyNamesDict
- ignoredPropertyNamesDict
- allowedCodingPropertyNamesDict
- ignoredCodingPropertyNamesDict
1.2.2 实现过程
Runtime
关联到对象上
// -mj_setupBlockReturnValue:key: 关键代码
objc_setAssociatedObject(self, key, block(), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 清空数据
[[self mj_classDictForKey:key] removeAllObjects];
block()
是一组属性名的集合,通过 -mj_setupBlockReturnValue:key:
方法设置,会将数组先关联到对象上,并将对应的缓存字典进行清空。
核心方法 - 获取需要配置的数组并缓存
+ (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key
{
MJExtensionSemaphoreCreate
MJ_LOCK(mje_signalSemaphore);
/// 取缓存数据,没有的话就要去获取一遍
NSMutableArray *array = [self mj_classDictForKey:key][NSStringFromClass(self)];
if (array == nil) {
// 创建、存储
[self mj_classDictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];
// 先检查是否有实现 NSObject+MJKeyValue 中的方法,有的话,获取并缓存之
// 这里只关心当心类的数据
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];
}];
}
MJ_UNLOCK(mje_signalSemaphore);
return array;
}
1.3 小结
NSObject+MJClass
分类主要作用有:
- 获取类的继承树
- 设置允许或者忽略的属性
2. MJPropertyType & MJPropertyKey
本来想解释 NSObject+MJProperty
分类,但是其中牵扯了其他的几个类,还是先讲几个封装类
2.1 MJPropertyType
这个类比较简单,主要就是用于封装属性对应的类型信息,是 id
类型,数字类型,还是 继承自 NSObject
的类型(包括自定义类和系统类)。
同样的也对其进行了缓存处理,利用的也是静态变量
这个类封装的信息是全局通用的, Class A中的
int
类型和Class B中的int
类型对应MJPropertyType
是同一个对象
布尔值存在 ”c“ 的情况是因为在80年代Objective-C诞生的年代,每一个bit都是很珍贵的,布尔被设计成signed char
2.1.1 核心方法 -setCode:
根据传进来的 code
值来决定这个属性的类型
参考资料:
这里有一个比较重要的属性 KVCDisabled
,影响后续的赋值和取值
2.2 MJPropertyKey
3. MJProperty
这个类主要是将属性进行封装和缓存。
3.1 初始化 & 缓存方法
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
MJProperty *propertyObj = objc_getAssociatedObject(self, property);
if (propertyObj == nil) {
propertyObj = [[self alloc] init];
propertyObj.property = property;
objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return propertyObj;
}
将 MJProperty
的实例关联到类对象上,实现缓存效果。
3.2 获取属性的名称、类型
- (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];
}
属性名称的获取比较好理解,属性的类型,如果不好理解的,需要再看一下官方Declared Properties的文章,看完就一清二楚了
3.3 属性值的存取
- 取值方法:
-valueForObject:
- 存值方法:
-setValue:forObject:
这两个方法都是利用 KVC
来实现的,在方法开始时都用到了 MJPropertyType
中的 KVCDisabled
属性,如果不支持 KVC
,那么不会进行存取值(取值时会返回null)。
3.4 -setOriginKey:forClass 映射方法(包括多级映射)
多级映射是框架提供的一种便利的功能,由于作者自己指定的规则,阅读代码的时候最好结合着例子来看会比较简单
@class MJBag;
@interface MJStudent : NSObject
@property (copy, nonatomic) NSString *ID;
@property (copy, nonatomic) NSString *otherName;
@property (copy, nonatomic) NSString *nowName;
@property (copy, nonatomic) NSString *oldName;
@property (copy, nonatomic) NSString *nameChangedTime;
@property (copy, nonatomic) NSString *desc;
@property (strong, nonatomic) MJBag *bag;
@property (strong, nonatomic) NSArray *books;
/** 32 bit bug */
@property (nonatomic) BOOL isAthlete;
@end
@implementation MJStudent
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
return @{@"ID" : @"id",
@"desc" : @"desciption",
@"oldName" : @"name.oldName",
@"nowName" : @"name.newName",
@"nameChangedTime" : @"name.info[1].nameChangedTime",
@"bag" : @"other.bag"
};
}
@end
// 1.定义一个字典
NSDictionary *dict = @{
@"id" : @"20",
@"desciption" : @"好孩子",
@"name" : @{
@"newName" : @"lufy",
@"oldName" : @"kitty",
@"info" : @[
@"test-data",
@{@"nameChangedTime" : @"2013-08-07"}
]
},
@"other" : @{
@"bag" : @{
@"name" : @"小书包",
@"price" : @100.7
}
}
};
// 2.将字典转为MJStudent模型
MJStudent *stu = [MJStudent mj_objectWithKeyValues:dict];
在字典转模型的过程中需要遍历所有的属性(包括自定义类型的父类),其中有一句
[property setOriginKey:[self mj_propertyKey:property.name] forClass:self];
就是在处理映射关系并缓存,其中 [self mj_propertyKey:property.name]
这句则是在获取映射的对应关系
映射关系的设置方式,在
NSObject+MJProperty
中
3.5 -propertyKeysWithStringKey: 映射规则
这里只说 -setOriginKey:forClass:
中 originKey
为字符串的时候,数组时感觉逻辑有问题,应该是作者写错了。
作者制定的字符串需要多级映射按照 .(逗号),进行分割,如果是需要映射到数组中的元素需要使用 [](中括号)。
3.6 小结
MJProperty
类的主要作用
- 将属性封装成
MJProperty
对象,包括属性名称和类型,并将其关联到类对象上 - 利用 KVC 进行存取值操作,要求属性支持 KVC 特性
MJProperty
有一个propertyKeysDict
和objectClassInArrayDict
字典,用于缓存映射关系和数组中元素类型
4. NSObject+MJProperty
通过这个分类名称就大概可以猜出都是关于属性的操作。
4.1 映射关系
MJExtension
支持更改对键值对的映射关系
4.1.1 配置映射关系
有2对(4个)方法可以配置映射关系
NSObject+MJKeyValue
提供的协议方法:
- +mj_replacedKeyFromPropertyName
- +mj_replacedKeyFromPropertyName121:
NSObject+MJProperty
提供的方法(这种方法会利用关联对象技术将配置信息关联到类对象上):
- +mj_setupReplacedKeyFromPropertyName:
- +mj_setupReplacedKeyFromPropertyName121:
4.1.2 -mj_propertyKey: 获取映射关系
这个方法就是上文中提到的关于映射关系的获取方法,按照下列顺序获取
-mj_replacedKeyFromPropertyName121:
(定义在MJKeyValue
协议中)- 利用
Runtime
从类对象中根据&MJReplacedKeyFromPropertyName121Key
获取关联的映射数据 -mj_replacedKeyFromPropertyName:
(定义在MJKeyValue
协议中)- 利用
Runtime
从类对象中根据&MJReplacedKeyFromPropertyNameKey
获取关联的映射数据
4.2 数组中的模型
MJExtension
支持解析数组中的自定义对象
4.2.1 配置数组中的模型
有2种方法可以进行配置:
- 通过协议方法
NSObject+MJKeyValue
中的+mj_objectClassInArray
进行配置 - 直接使用
NSObject+MJProperty
提供的+mj_setupObjectClassInArray
进行配置(这种方法会利用关联对象技术将配置信息关联到类对象上)
映射关系和数组中的模型,在将属性封装成
MJProperty
会进行缓存(存储在propertyKeysDict
和objectClassInArrayDict
两个字典中)
4.3 新值替换
映射关系是用新的 key 去取出原来的 value,新值替换就是用原来的 key,取出不同的 value
- (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;
}
这个是作者的实例中截取的代码,可以看出就是将 oldValue 换成了一个 newValue
4.4 获取所有属性的MJPerperty封装对象
+mj_enumerateProperties:
遍历所有的属性,以 MJPerperty
的形式返回
4.4.1 属性封装过程
- 获取的过程存在线程安全问题,先加个锁
- 对于一个自定义对象存在多次获取的可能,加个缓存机制,可以先从缓存中获取
- 调用的是
+mj_enumerateClasses:
, 所以可能不会遍历完继承树中所有的类 - 得到继承树中的
Class
后,利用Runtime
来获取其对应的所有属性 - 将属性从
objc_property_t
结构体封装成MJProperty
对象 - 如果当前属性类型是来自系统的
Foundation
框架和NSObject
协议中定义的属性(此处是协议),那么就忽略这个属性 - 如果有映射关系,配置并缓存新的键值关系
- 如果有模型数组,配置并缓存数组信息
4.5 小结
主要是针对 Property
进行一些可定制的配置,包括更改键值和模型数组,此外最主要的功能是 获取并封装该类在继承树中可用的所有属性
5. NSObject+MJKeyValue
这个分类包含了两大部分:
MJKeyValue
协议NSObject+MJKeyValue
分类方法
这里包含的就是平时开发中接触到的各类API,主要功能包括:
- 模型转字典
- 模型数组转字典数组
- 字典转模型
- 字典数组转模型数组
MJExtension
提供了很多API,但是最后归结到一起后就是2个方法
// 字典转模型
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context;
// 模型转字典
- (NSMutableDictionary *)mj_keyValuesWithKeys:(NSArray *)keys ignoredKeys:(NSArray *)ignoredKeys;
字典转模型
// 字典转模型
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context;
- 判断是否能否转成字典格式
- 获取需要转换的
key
的集合和需要忽略的key
的集合 - 如果有需要,获取字符串格式化数字时可能用到的时区
- 遍历所有的属性(包括父类,相同的属性,子类优先)
- 如果存在白名单集合,那么不在白名单中的属性会被忽略
- 如果存在黑名单集合,那么在黑名单中的属性会被忽略
- 取出属性的值
- 值过滤,使用旧
key
取出自定义的新值 - 值的类型不满足属性类型时,进行转换
模型转字典
// 模型转字典
- (NSMutableDictionary *)mj_keyValuesWithKeys:(NSArray *)keys ignoredKeys:(NSArray *)ignoredKeys;
- 判断是否能否转成字典格式
- 获取需要转换的
key
的集合和需要忽略的key
的集合 - 属性取值
- 如果是自定义类型,调用
mj_keyValues
进行转换 - 如果是数组,调用
mj_keyValuesArrayWithObjectArray
进行转换 NSURL
特别处理- 处理NULL
6.总结
从该框架中,可以学习的地方:
- 对于类的功能划分清晰
- 两个类之间的依赖尽可能小
- 针对接口编程(
MJKeyValue
协议),框架中还提供了一个静态设置方法,但是优先级低于协议方法