MJExtension中关于NSCoding的一些源码分析
写在前面
上一篇:runtime:一句代码实现对象NSCoding主要实现了运行时遍历对象属性来归档的功能,类似于NSObject+MJCoding
的中的实现,但是对于MJExtension框架中的分析还不够透彻,这篇我尝试对MJExtension
中的关于NSCoding
的部分进行解读分析。主要代码来自MJExtension
中的NSObject+MJClass
和NSObject+MJCoding
。
NSObject+MJClass
NSObject+MJClass
中主要有以下方法:

-
遍历所有的类
mj_enumerateClasses
mj_enumerateAllClasses -
关于黑白名单的配置方法有两种:
通过block来return一个数组。
实体类中重写对应的方法,方法中返回一个数组
特别注意:两种的写法不同,但是作用是一样的!例如下这两个:
//通过block返回数组
typedef NSArray * (^MJAllowedPropertyNames)();
+ (void)mj_setupAllowedPropertyNames:(MJAllowedPropertyNames)allowedPropertyNames;
//通过方法返回一个数组
+ (NSMutableArray *)mj_totalAllowedPropertyNames;
mj_enumerateClasses和mj_enumerateAllClasses两个方法的区别
通过查看代码,发现mj_enumerateClasses
中多了这么一行
if ([MJFoundation isClassFromFoundation:c]) break;
查看位于MJFoundation
中的isClassFromFoundation
这个方法的具体实现:
+ (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;
}
+ (NSSet *)foundationClasses
{
if (foundationClasses_ == nil) {
// 集合中没有NSObject,因为几乎所有的类都是继承自NSObject,具体是不是NSObject需要特殊判断
foundationClasses_ = [NSSet setWithObjects:
[NSURL class],
[NSDate class],
[NSValue class],
[NSData class],
[NSError class],
[NSArray class],
[NSDictionary class],
[NSString class],
[NSAttributedString class], nil];
}
return foundationClasses_;
}
通过以上两个方法,相信可以很明确的确定mj_enumerateClasses把NSObject
和NSManagedObject
,以及foundationClasses
集合中的诸如NSURL
等排除在外,只能遍历得到自定义的Class。而mj_enumerateAllClasses则是可以得到包含了系统的Class,例如NSObject
。从名字我们也可以明显看出来。
配置黑白名单,有两种方式:
- 通过block来return一个数组;
- 重写配置方法,return一个数组
通过block传入,白名单和黑名单的数据设置
//属性白名单配置(用于模型字典转化)
+ (void)mj_setupAllowedPropertyNames:(MJAllowedPropertyNames)allowedPropertyNames;
//属性黑名单配置
+ (void)mj_setupIgnoredPropertyNames:(MJIgnoredPropertyNames)ignoredPropertyNames;
//属性归档白名单配置(用于归档)
+ (void)mj_setupAllowedCodingPropertyNames:(MJAllowedCodingPropertyNames)allowedCodingPropertyNames;
//属性归档黑名单配置
+ (void)mj_setupIgnoredCodingPropertyNames:(MJIgnoredCodingPropertyNames)ignoredCodingPropertyNames;
从代码可以发现,以上四个方法,调用了同一个方法。而且注释了“内部使用”
#pragma mark - 内部使用
+ (void)mj_setupBlockReturnValue:(id (^)())block key:(const char *)key;
原来是因为,业务逻辑是一致的,使用过了key来作为四种情况的区分,内部已经定义好了key,分别对应四种情况:
static const char MJAllowedPropertyNamesKey = '\0';
static const char MJIgnoredPropertyNamesKey = '\0';
static const char MJAllowedCodingPropertyNamesKey = '\0';
static const char MJIgnoredCodingPropertyNamesKey = '\0';
内部使用的方法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];
}
objc_setAssociatedObject
是不是很眼熟!没错,这个是用来做属性关联的,又是key
又是objc_setAssociatedObject
的!
原来就是把从外部接收的block
中的数组,动态关联到当前的对象。关联完毕,再清空当前的数组removeAllObjects
,以便于下次的循环获取到的对象重新赋值,达到不重复关联数据的目的。
通过重写方法返回数组,白名单和黑名单的数据的设置
在NSObject+MJClass
中提供了几个方法,分别如下:
//属性白名单配置(用于模型字典转化)
+ (NSMutableArray *)mj_totalAllowedPropertyNames;
//归档属性白名单配置
+ (NSMutableArray *)mj_totalIgnoredPropertyNames;
//属性黑名单配置(用于归档)
+ (NSMutableArray *)mj_totalAllowedCodingPropertyNames;
//归档属性黑名单配置
+ (NSMutableArray *)mj_totalIgnoredCodingPropertyNames;
以上四个方法,跟外部传入block返回数组来进行数据配置的情况一致,也是调用同一个方法mj_totalObjectsWithSelector: key:
,只不过传入之后是这么操作:
+ (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;
}
可以看到,内部调用主要是调用了这个方法:
+ (NSMutableDictionary *)dictForKey:(const void *)key
{
@synchronized (self) {
if (key == &MJAllowedPropertyNamesKey) return allowedPropertyNamesDict_;
if (key == &MJIgnoredPropertyNamesKey) return ignoredPropertyNamesDict_;
if (key == &MJAllowedCodingPropertyNamesKey) return allowedCodingPropertyNamesDict_;
if (key == &MJIgnoredCodingPropertyNamesKey) return ignoredCodingPropertyNamesDict_;
return nil;
}
}
内部用于存储数据的四个字典:
static NSMutableDictionary *allowedPropertyNamesDict_;
static NSMutableDictionary *ignoredPropertyNamesDict_;
static NSMutableDictionary *allowedCodingPropertyNamesDict_;
static NSMutableDictionary *ignoredCodingPropertyNamesDict_;
@synchronized (self) {}这个语法,相当于
NSLock
,为了避免多线程访问时的问题。更多戳这里
仔细的分析下这个方法mj_totalObjectsWithSelector: key:
- 从关联属性中取出,如果有值的话,直接返回。(前提是外部调用了block的方法来设置黑白名单的方法,并传值进来)。
对应代码:
NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
if (array) return array;
-
dictForKey
没有返回值,说明外部没有通过block来设置名单。创建一个空的数组,并根据key值设置给四个字典中的对应的一个。通过respondsToSelector
动态获取当前继承自NSObject
的类中的重写的方法中返回的数组subArray,此时,该数组已经关联到其中一个字典了。
对应代码:
// 创建、存储
[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];
}
}
- 通过上面的操作,外部重写方法所返回的数组,已经作为关联属性存储起来了,此时只需要通过
objc_getAssociatedObject
通过key值取出来,返回:
对应代码:
[self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
NSArray *subArray = objc_getAssociatedObject(c, key);
[array addObjectsFromArray:subArray];
}];
return array;
NSObject+MJCoding
NSObject+MJCoding.h
#import <Foundation/Foundation.h>
#import "MJExtensionConst.h"
/**
* Codeing协议
*/
@protocol MJCoding <NSObject>
@optional
/**
* 这个数组中的属性名才会进行归档
*/
+ (NSArray *)mj_allowedCodingPropertyNames;
/**
* 这个数组中的属性名将会被忽略:不进行归档
*/
+ (NSArray *)mj_ignoredCodingPropertyNames;
@end
@interface NSObject (MJCoding) <MJCoding>
/**
* 解码(从文件中解析对象)
*/
- (void)mj_decode:(NSCoder *)decoder;
/**
* 编码(将对象写入文件中)
*/
- (void)mj_encode:(NSCoder *)encoder;
@end
/**
归档的实现
*/
#define MJCodingImplementation \
- (id)initWithCoder:(NSCoder *)decoder \
{ \
if (self = [super init]) { \
[self mj_decode:decoder]; \
} \
return self; \
} \
\
- (void)encodeWithCoder:(NSCoder *)encoder \
{ \
[self mj_encode:encoder]; \
}
#define MJExtensionCodingImplementation MJCodingImplementation
NSObject+MJCoding.h
文件中看起来很长,其实分为三个部分:
- MJCoding协议;
其中有两个方法:
mj_allowedCodingPropertyNames
和mj_ignoredCodingPropertyNames
看注释可以得知,是对外暴露的关于黑白名单的设置,而且此分类已经帮我们引入了@interface NSObject (MJCoding) <MJCoding>
,外部实现不再需要重复引入该协议,实现上述两个方法即可,当然,看业务需求,你是完全自由设置的。例如:
#import "Father.h"
@interface Son : Father
@property(nonatomic,copy)NSString *sonProperty;
@end
#import "Son.h"
#import <NSObject+MJCoding.h>
@implementation Son
//归档实现
MJCodingImplementation
//黑名单设置
+(NSArray*)mj_ignoredCodingPropertyNames{
return @[@"sonProperty"];
}
@end
- 解码、编码两个方法;
两个方法:
mj_decode
和mj_encode
其实都是通过mj_enumerateProperties
遍历自定义对象的属性,内部已经实现多继承的属性对象遍历,然后逐个去对比黑白名单,从而得到需要编码和解码的属性。
- (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];
}];
}
- (void)mj_decode:(NSCoder *)decoder
{
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 = [decoder decodeObjectForKey:property.name];
if (value == nil) { // 兼容以前的MJExtension版本
value = [decoder decodeObjectForKey:[@"_" stringByAppendingString:property.name]];
}
if (value == nil) return;
[property setValue:value forObject:self];
}];
}
- 解码编码两个方法的宏抽取。
说到底,
mj_decode
和mj_encode
其实都是对实现系统NSCodingd 的两个方法做了封装,最终还是需要写到-(instancetype)initWithCoder:(NSCoder *)aDecoder
和-(void) encodeWithCoder:(NSCoder *)aCoder
的内部,不过对于每个需要归档解档的Class来说,代码都是一致的,所以把这一致的抽取成一句宏,之后调用,就只需要调用这一句了,MJExtensionCodingImplementation
和MJCodingImplementation
都可以。具体实现:
#define MJCodingImplementation \
- (id)initWithCoder:(NSCoder *)decoder \
{ \
if (self = [super init]) { \
[self mj_decode:decoder]; \
} \
return self; \
} \
\
- (void)encodeWithCoder:(NSCoder *)encoder \
{ \
[self mj_encode:encoder]; \
}
#define MJExtensionCodingImplementation MJCodingImplementation
最后
其中应该需要补充,有些解释不太清楚,后面会做补充。
如果您觉得本文对您有一定的帮助,请随手点个喜欢,十分感谢!🌹🌹🌹