MJExtension源码解析

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.hMJProperty.hMJPropertyKey.hMJPropertyType.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];`
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容