iOS相关ios

MJExtension中关于NSCoding的一些源码分析

2017-12-14  本文已影响35人  上发条的树

写在前面

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

NSObject+MJClass

NSObject+MJClass中主要有以下方法:

NSObject+MJClass.png
//通过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把NSObjectNSManagedObject,以及foundationClasses集合中的诸如NSURL等排除在外,只能遍历得到自定义的Class。而mj_enumerateAllClasses则是可以得到包含了系统的Class,例如NSObject。从名字我们也可以明显看出来。

配置黑白名单,有两种方式:

通过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:

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;

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文件中看起来很长,其实分为三个部分:

其中有两个方法:mj_allowedCodingPropertyNamesmj_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_decodemj_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_decodemj_encode其实都是对实现系统NSCodingd 的两个方法做了封装,最终还是需要写到-(instancetype)initWithCoder:(NSCoder *)aDecoder-(void) encodeWithCoder:(NSCoder *)aCoder的内部,不过对于每个需要归档解档的Class来说,代码都是一致的,所以把这一致的抽取成一句宏,之后调用,就只需要调用这一句了,MJExtensionCodingImplementationMJCodingImplementation都可以。具体实现:

#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

最后

其中应该需要补充,有些解释不太清楚,后面会做补充。

如果您觉得本文对您有一定的帮助,请随手点个喜欢,十分感谢!🌹🌹🌹

上一篇 下一篇

猜你喜欢

热点阅读