美文网首页iOS相关ios
MJExtension中关于NSCoding的一些源码分析

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

作者: 上发条的树 | 来源:发表于2017-12-14 20:49 被阅读35次

写在前面

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

NSObject+MJClass

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

NSObject+MJClass.png
  • 遍历所有的类
    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把NSObjectNSManagedObject,以及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_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

最后

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

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

相关文章

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

    写在前面 上一篇:runtime:一句代码实现对象NSCoding主要实现了运行时遍历对象属性来归档的功能,类似于...

  • MJExtension框架源码分析

    MJExtension框架源码分析 MJExtension框架源码分析

  • MJExtension源码分析

    原始方法 实现我们的模型是这样的,只有2个属性 使用最原始的方法把json->Models时,代码如下 稍微优化下...

  • MJExtension分析

    MJExtension不看源码不知道,竟然是国产的,看着66的中文注释,满满的幸福: 源码分析MJExtensio...

  • MJExtension框架源码分析

    iOS开发中经常会用到数据和模型的互相转换,大致有两种转换方式:1.手动写转换的代码,2.利用开源库进行转换。常用...

  • iOS开发MJExtension源码阅读笔记

    字典/JSON转模型用过MJExtension这个轻量级框架,现在重读MJExtension源码,顺便记录一下

  • [Java源码][并发J.U.C]---解析ReentrantR

    前言 本文将分析读写锁ReentrantReadWriteLock的源码,也会在分析中穿插一些例子来加深对源码的理...

  • MJExtension 初步学习

    MJExtension 的一些使用方法 1.在头文件中引入"MJExtension.h":解决属性中的命名和服务器...

  • HashMap源码分析

    一、前言 上篇简单分析了下HashTable,本篇就分析HashMap的源码,对于HashMap源码中涉及到的一些...

  • brpc之rpc流程分析(上)

    之前关于brpc的几篇分析:brpc之mutex源码分析brpc之ResourcePool源码分析brpc之bth...

网友评论

    本文标题:MJExtension中关于NSCoding的一些源码分析

    本文链接:https://www.haomeiwen.com/subject/cnnkwxtx.html