MJExtension源码解析

作者: 纸简书生 | 来源:发表于2016-07-26 15:15 被阅读3001次

    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];`
    

    相关文章

      网友评论

      • 上发条的树:要是排版能好点就好咯...
      • 1江春水:1、+load;方法是运行时决定的,调用的时间是不确定的吧。
        2、MJProperty *propertyObj = objc_getAssociatedObject(self, property);这句话是动态给类添加属性,也就是说,给类添加了一个属性名为property的MJProperty类型属性,这个地方完全错误,应该是根据property从self类中取值,恳请作者更改指正!:blush:
        蛋壳儿:load是程序在runtime环境介入后就会执行的,加载类文件时会调用,调用时间早,作者表达的不是很严谨,但大体没错
      • 宋鸿康iOS:牛逼,断言,方法弃用
      • 小怡情ifelse:躺在床上看 不能手动写下 明天抽空仔细看下
      • 态度哥:虽然没有看完 但是写的很详细,赞

      本文标题:MJExtension源码解析

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