MJExtension 源码解析

1. NSObject+MJClass

为基类添加了一个 Class 相关的分类,用于获取设置所有关于 Class 的配置。

1.1 核心方法 - 遍历类的继承树

/**
 *  遍历所有的类
 */
+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration;
+ (void)mj_enumerateAllClasses:(MJClassesEnumeration)enumeration;

这两个方法都会遍历类的继承树,区别在于:

  • mj_enumerateAllClasses 会遍历继承树中所有的类,直到 NSObject 或者 NSManagedObject 为止。
  • mj_enumerateClasses 如果其父类为 Foundation 中类的子类,只遍历到当前类

1.2 白名单和黑名单

这个当前类的属性设置到白名单或者黑名单字典中
NSObject+MJKeyValue 核心类中,进行模型和字典转换时会用到

白名单设置时不能返回长度为0的数组
如果白名单和黑名单都有设置,那么就会取并集

另外还有一对 归档白名单和黑名单,基本上是类似的,用于归解档

1.2.1 实现原理

设置黑白名单有2中方式:

  • 直接调用 NSObject+MJClass 对应的方法, 这种方式会将数组利用 Runtime 关联到类对象上
[CZAnimal mj_setupIgnoredPropertyNames:^NSArray *{
    return @[@"type"];
}];
  • 利用 NSObject+MJKeyValue 提供的方法,这个方式是在运行时获取需要设置的数组
+ (NSArray *)mj_ignoredPropertyNames {
    return @[@"type"];
}

不管是哪种方式,在获取过一次后,就会被缓存起来,内部创建了4个静态局部字典变量用来缓存白名单和黑名单的数据

  • allowedPropertyNamesDict
  • ignoredPropertyNamesDict
  • allowedCodingPropertyNamesDict
  • ignoredCodingPropertyNamesDict

1.2.2 实现过程

Runtime 关联到对象上

// -mj_setupBlockReturnValue:key: 关键代码
objc_setAssociatedObject(self, key, block(), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// 清空数据
[[self mj_classDictForKey:key] removeAllObjects];

block() 是一组属性名的集合,通过 -mj_setupBlockReturnValue:key: 方法设置,会将数组先关联到对象上,并将对应的缓存字典进行清空。

核心方法 - 获取需要配置的数组并缓存

+ (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key
{
    MJExtensionSemaphoreCreate
    MJ_LOCK(mje_signalSemaphore);
    /// 取缓存数据,没有的话就要去获取一遍
    NSMutableArray *array = [self mj_classDictForKey:key][NSStringFromClass(self)];
    if (array == nil) {
        // 创建、存储
        [self mj_classDictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];
        
        // 先检查是否有实现 NSObject+MJKeyValue 中的方法,有的话,获取并缓存之
        // 这里只关心当心类的数据
        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];
        }];
    }
    MJ_UNLOCK(mje_signalSemaphore);
    return array;
}

1.3 小结

NSObject+MJClass 分类主要作用有:

  1. 获取类的继承树
  2. 设置允许或者忽略的属性

2. MJPropertyType & MJPropertyKey

本来想解释 NSObject+MJProperty 分类,但是其中牵扯了其他的几个类,还是先讲几个封装类

2.1 MJPropertyType

这个类比较简单,主要就是用于封装属性对应的类型信息,是 id 类型,数字类型,还是 继承自 NSObject 的类型(包括自定义类和系统类)。

同样的也对其进行了缓存处理,利用的也是静态变量

这个类封装的信息是全局通用的, Class A中的 int 类型和Class B中的 int 类型对应 MJPropertyType 是同一个对象
布尔值存在 ”c“ 的情况是因为在80年代Objective-C诞生的年代,每一个bit都是很珍贵的,布尔被设计成signed char

2.1.1 核心方法 -setCode:

根据传进来的 code 值来决定这个属性的类型

参考资料:

这里有一个比较重要的属性 KVCDisabled,影响后续的赋值和取值

2.2 MJPropertyKey

3. MJProperty

这个类主要是将属性进行封装缓存

3.1 初始化 & 缓存方法

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

MJProperty 的实例关联到类对象上,实现缓存效果。

3.2 获取属性的名称、类型

- (void)setProperty:(objc_property_t)property
{
    _property = property;
    
    MJExtensionAssertParamNotNil(property);
    
    // 1.属性名
    _name = @(property_getName(property));
    
    // 2.成员类型
    NSString *attrs = @(property_getAttributes(property));
    NSUInteger dotLoc = [attrs rangeOfString:@","].location;
    NSString *code = nil;
    NSUInteger loc = 1;
    if (dotLoc == NSNotFound) { // 没有,
        code = [attrs substringFromIndex:loc];
    } else {
        code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)];
    }
    _type = [MJPropertyType cachedTypeWithCode:code];
}

属性名称的获取比较好理解,属性的类型,如果不好理解的,需要再看一下官方Declared Properties的文章,看完就一清二楚了

3.3 属性值的存取

  • 取值方法:-valueForObject:
  • 存值方法:-setValue:forObject:

这两个方法都是利用 KVC 来实现的,在方法开始时都用到了 MJPropertyType 中的 KVCDisabled 属性,如果不支持 KVC,那么不会进行存取值(取值时会返回null)。

3.4 -setOriginKey:forClass 映射方法(包括多级映射)

多级映射是框架提供的一种便利的功能,由于作者自己指定的规则,阅读代码的时候最好结合着例子来看会比较简单

@class MJBag;

@interface MJStudent : NSObject
@property (copy, nonatomic) NSString *ID;
@property (copy, nonatomic) NSString *otherName;
@property (copy, nonatomic) NSString *nowName;
@property (copy, nonatomic) NSString *oldName;
@property (copy, nonatomic) NSString *nameChangedTime;
@property (copy, nonatomic) NSString *desc;
@property (strong, nonatomic) MJBag *bag;
@property (strong, nonatomic) NSArray *books;

/** 32 bit bug */
@property (nonatomic) BOOL isAthlete;
@end

@implementation MJStudent
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
    return @{@"ID" : @"id",
             @"desc" : @"desciption",
             @"oldName" : @"name.oldName",
             @"nowName" : @"name.newName",
             @"nameChangedTime" : @"name.info[1].nameChangedTime",
             @"bag" : @"other.bag"
             };
}
@end


// 1.定义一个字典
NSDictionary *dict = @{
    @"id" : @"20",
    @"desciption" : @"好孩子",
    @"name" : @{
        @"newName" : @"lufy",
        @"oldName" : @"kitty",
        @"info" : @[
            @"test-data",
            @{@"nameChangedTime" : @"2013-08-07"}
        ]
    },
    @"other" : @{
        @"bag" : @{
            @"name" : @"小书包",
            @"price" : @100.7
        }
    }
};

// 2.将字典转为MJStudent模型
MJStudent *stu = [MJStudent mj_objectWithKeyValues:dict];

在字典转模型的过程中需要遍历所有的属性(包括自定义类型的父类),其中有一句

[property setOriginKey:[self mj_propertyKey:property.name] forClass:self];

就是在处理映射关系并缓存,其中 [self mj_propertyKey:property.name] 这句则是在获取映射的对应关系

映射关系的设置方式,在 NSObject+MJProperty

3.5 -propertyKeysWithStringKey: 映射规则

这里只说 -setOriginKey:forClass:originKey 为字符串的时候,数组时感觉逻辑有问题,应该是作者写错了。

作者制定的字符串需要多级映射按照 .(逗号),进行分割,如果是需要映射到数组中的元素需要使用 [](中括号)

3.6 小结

MJProperty 类的主要作用

  • 将属性封装成 MJProperty 对象,包括属性名称和类型,并将其关联到类对象上
  • 利用 KVC 进行存取值操作,要求属性支持 KVC 特性
  • MJProperty 有一个 propertyKeysDictobjectClassInArrayDict 字典,用于缓存映射关系和数组中元素类型

4. NSObject+MJProperty

通过这个分类名称就大概可以猜出都是关于属性的操作。

4.1 映射关系

MJExtension 支持更改对键值对的映射关系

4.1.1 配置映射关系

有2对(4个)方法可以配置映射关系
NSObject+MJKeyValue 提供的协议方法:

  • +mj_replacedKeyFromPropertyName
  • +mj_replacedKeyFromPropertyName121:

NSObject+MJProperty 提供的方法(这种方法会利用关联对象技术将配置信息关联到类对象上):

  • +mj_setupReplacedKeyFromPropertyName:
  • +mj_setupReplacedKeyFromPropertyName121:

4.1.2 -mj_propertyKey: 获取映射关系

这个方法就是上文中提到的关于映射关系的获取方法,按照下列顺序获取

  • -mj_replacedKeyFromPropertyName121: (定义在 MJKeyValue 协议中)
  • 利用 Runtime 从类对象中根据 &MJReplacedKeyFromPropertyName121Key 获取关联的映射数据
  • -mj_replacedKeyFromPropertyName:(定义在 MJKeyValue 协议中)
  • 利用 Runtime 从类对象中根据 &MJReplacedKeyFromPropertyNameKey 获取关联的映射数据

4.2 数组中的模型

MJExtension 支持解析数组中的自定义对象

4.2.1 配置数组中的模型

有2种方法可以进行配置:

  • 通过协议方法 NSObject+MJKeyValue 中的 +mj_objectClassInArray 进行配置
  • 直接使用 NSObject+MJProperty 提供的 +mj_setupObjectClassInArray进行配置(这种方法会利用关联对象技术将配置信息关联到类对象上)

映射关系和数组中的模型,在将属性封装成 MJProperty 会进行缓存(存储在 propertyKeysDictobjectClassInArrayDict 两个字典中)

4.3 新值替换

映射关系是用新的 key 去取出原来的 value,新值替换就是用原来的 key,取出不同的 value

- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property
{
    if ([property.name isEqualToString:@"publisher"]) {
        if (oldValue == nil) return @"";
    } else if (property.type.typeClass == [NSDate class]) {
        NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
        fmt.dateFormat = @"yyyy-MM-dd";
        return [fmt dateFromString:oldValue];
    }
    
    return oldValue;
}

这个是作者的实例中截取的代码,可以看出就是将 oldValue 换成了一个 newValue

4.4 获取所有属性的MJPerperty封装对象

+mj_enumerateProperties: 遍历所有的属性,以 MJPerperty 的形式返回

4.4.1 属性封装过程

  • 获取的过程存在线程安全问题,先加个锁
  • 对于一个自定义对象存在多次获取的可能,加个缓存机制,可以先从缓存中获取
  • 调用的是 +mj_enumerateClasses:, 所以可能不会遍历完继承树中所有的类
  • 得到继承树中的 Class 后,利用 Runtime 来获取其对应的所有属性
  • 将属性从 objc_property_t 结构体封装成 MJProperty 对象
  • 如果当前属性类型是来自系统的 Foundation 框架和 NSObject协议中定义的属性(此处是协议),那么就忽略这个属性
  • 如果有映射关系,配置并缓存新的键值关系
  • 如果有模型数组,配置并缓存数组信息

4.5 小结

主要是针对 Property 进行一些可定制的配置,包括更改键值和模型数组,此外最主要的功能是 获取并封装该类在继承树中可用的所有属性

5. NSObject+MJKeyValue

这个分类包含了两大部分:

  • MJKeyValue 协议
  • NSObject+MJKeyValue 分类方法

这里包含的就是平时开发中接触到的各类API,主要功能包括:

  • 模型转字典
  • 模型数组转字典数组
  • 字典转模型
  • 字典数组转模型数组

MJExtension 提供了很多API,但是最后归结到一起后就是2个方法

// 字典转模型
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context;
// 模型转字典
- (NSMutableDictionary *)mj_keyValuesWithKeys:(NSArray *)keys ignoredKeys:(NSArray *)ignoredKeys;

字典转模型

// 字典转模型
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context;
  • 判断是否能否转成字典格式
  • 获取需要转换的 key 的集合和需要忽略的 key 的集合
  • 如果有需要,获取字符串格式化数字时可能用到的时区
  • 遍历所有的属性(包括父类,相同的属性,子类优先)
  • 如果存在白名单集合,那么不在白名单中的属性会被忽略
  • 如果存在黑名单集合,那么在黑名单中的属性会被忽略
  • 取出属性的值
  • 值过滤,使用旧 key 取出自定义的新值
  • 值的类型不满足属性类型时,进行转换

模型转字典

// 模型转字典
- (NSMutableDictionary *)mj_keyValuesWithKeys:(NSArray *)keys ignoredKeys:(NSArray *)ignoredKeys;
  • 判断是否能否转成字典格式
  • 获取需要转换的 key 的集合和需要忽略的 key 的集合
  • 属性取值
  • 如果是自定义类型,调用 mj_keyValues 进行转换
  • 如果是数组,调用 mj_keyValuesArrayWithObjectArray 进行转换
  • NSURL 特别处理
  • 处理NULL

6.总结

从该框架中,可以学习的地方:

  • 对于类的功能划分清晰
  • 两个类之间的依赖尽可能小
  • 针对接口编程(MJKeyValue 协议),框架中还提供了一个静态设置方法,但是优先级低于协议方法
posted @ 2022-09-26 09:47  小小个子大个头  阅读(153)  评论(0编辑  收藏  举报