[Edward炼金屋] MJExtension阅读笔记

写在前头:该系列仅仅只是个人的阅读笔记而已,不会做过多的讲解和分析。

知识点:

在设计上为了提高转化效率大量的使用了缓存,项目中缓存的方案有两种:

  1. 利用关联对象的方法,直接在类对象上添加关联对象。
    举个例子,作者在MJProperty类中使用了这个技巧。由于对于同一个属性objc_property_t生成过的MJProperty对象的内容都是相同的,因此作者对其进行了缓存避免重复的执行生成逻辑。作者巧妙的利用了属性property作为key。属性为一个结构体指针。类的属性的地址在编译器就决定了,而且是唯一的。它可以很好的充当key的角色。
+ (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;
}
  1. 使用了静态字典对象,在+initialize或者+load中对静态字典对象进行初始化。
    作者用这个方案来缓存不同类的信息,这个字典的key为类名字符串,而value为其内容。
    同样举个例子,在NSObject+MJClass.h中存储了需要属性的黑白名单,白名单中的属性会进行字典和模型的转换,而黑名单中的属性则会被忽略。白名单为则表示全部参与转换。
//代码做了简化
static NSMutableDictionary *allowedPropertyNamesDict_;
static NSMutableDictionary *ignoredPropertyNamesDict_;

@implementation NSObject (MJClass)

+ (void)load
{
    allowedPropertyNamesDict_ = [NSMutableDictionary dictionary];
    ignoredPropertyNamesDict_ = [NSMutableDictionary dictionary];
}

#pragma mark - 属性白名单配置
+ (void)mj_setupAllowedPropertyNames:(MJAllowedPropertyNames)allowedPropertyNames;
{
    NSMutableArray *array = allowedPropertyNamesDict_[NSStringFromClass(self)];
    if (array) return array;
    
    // 创建、存储
    allowedPropertyNamesDict_[NSStringFromClass(self)] = array = [NSMutableArray array];
    
    ....

    return array;
    
}

+ (NSMutableArray *)mj_totalAllowedPropertyNames
{
    NSMutableArray *array = ignoredPropertyNamesDict_[NSStringFromClass(self)];
    if (array) return array;
    
    // 创建、存储
    ignoredPropertyNamesDict_[NSStringFromClass(self)] = array = [NSMutableArray array];
    
    ....

    return array;
}

@end

使用宏定义来使得类具有归档的作用

作者写了一个宏MJExtensionCodingImplementation,想要一个类支持归档,只需要在实现函数中添加这个宏即可。

@implementation MJBag
// NSCoding实现
MJExtensionCodingImplementation

@end

作者是怎么做到的呢?我们只要展开这个宏就可以知道了。这个宏帮我们实现了- (id)initWithCoder:(NSCoder *)decoder- (void)encodeWithCoder:(NSCoder *)encoder方法。在这两个方法中,分别调用了作者写的编码和解码的方法,在方法中会遍历所有的属性进行操作。

#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

属性的字符串描述(the attribute string of a property)

我们可以通过runtime库中const char *property_getAttributes(objc_property_t property)方法获得属性的类型描述和属性名称。我们可以使用字符串字面量方法@()将c字符串转成NSString类型。

属性的字符串描述:T + 变量类型编码 + 属性类型编码 + V + 属性名称    

官方连接:Property Type String,Type Encodings

不足之处:

存在的多线程问题

MJExtension似乎并不支持多线程的转换,我在阅读源码中发现了两个多线程的问题。

  • 多线程问题1
    我构造了这么一个场景在两个线程同时对同一个模型进行转换,但是使用的字典不相同。代码如下,为了方便查看,我在 //后给出了打印结果。
- (void)testMutilThread1
{
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        NSDictionary *dict = @{
                               @"name":@"name",
                               @"price":@(2)
                               };
        [MJBag mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
            return @{
                     @"price":@"price"
                     };
        }];
        MJBag *bag =  [MJBag mj_objectWithKeyValues:dict];
        NSLog(@"%@",bag);        // name:name,price:0.000000
    });
    
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        NSDictionary *dict = @{
                               @"name":@"name",
                               @"price2":@(3)
                               };
        [MJBag mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
            return @{
                        @"price":@"price2"
                     };
        }];
        MJBag *bag =  [MJBag mj_objectWithKeyValues:dict];
        NSLog(@"%@",bag);      //name:name,price:3.000000
    });
}

我们可以看到第一个对象的结果是错误的。究其原因在于:对于同一个类的属性和字典的映射关系表在程序中只会存在一份。我们在第一个线程设置好映射关系和执行转换之间,第二个线程重新设置了映射关系,这导致了第一个字典使用了第二个字典的映射关系,从而转换错误。

  • 多线程问题2
    我们知道当我们遇到多个线程同时修改同一个字典对象的时候需要加锁。可是我在代码多个地方发现了问题,特别是对于缓存字典的操作上。我有点怀疑作者的锁加错了位置,作者只在字典的获取上加了锁,由于字典的创建在这之前已经完成,似乎多线程访问并没有问题。对于这个问题,我没有做实验,目前还处在对于代码的分析层面上。

一个模型对应多个字典效率降低的问题

之前说过作者使用字典做缓存来加快模型字典的转换效率。其中一个重要的缓存就是类的属性列表及其与字典的映射关系。作者采用字典作为缓存的容器,每一个类只有一个缓存数据。这导致当一个模型需要被用在不同的位置,需要使用不同的字典来转换,那每次转换都需要重新设置映射关系,导致缓存的失效。
更糟糕的是作者采用了一刀切的方式来清理缓存。我们来看下设置映射关系的代码。只要一个类重新设着了映射关系,那么所有的缓存都会被清理。这并不是一个聪明的做法。

+ (void)mj_setupReplacedKeyFromPropertyName:(MJReplacedKeyFromPropertyName)replacedKeyFromPropertyName
{
    //设置新的属性和字典的映射关系
    [self mj_setupBlockReturnValue:replacedKeyFromPropertyName key:&MJReplacedKeyFromPropertyNameKey];
    //清理全部的缓存
    [[self dictForKey:&MJCachedPropertiesKey] removeAllObjects];
}

疑惑

不同分类有相同的类方名,为什么不会出现冲突的情况呢?

后面查查 runtime的源码找找原因。

@implementation NSObject (MJClass)

+ (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;
    }
}

......

@end

@implementation NSObject (Property)

+ (NSMutableDictionary *)dictForKey:(const void *)key
{
    @synchronized (self) {
        if (key == &MJReplacedKeyFromPropertyNameKey) return replacedKeyFromPropertyNameDict_;
        if (key == &MJReplacedKeyFromPropertyName121Key) return replacedKeyFromPropertyName121Dict_;
        if (key == &MJNewValueFromOldValueKey) return newValueFromOldValueDict_;
        if (key == &MJObjectClassInArrayKey) return objectClassInArrayDict_;
        if (key == &MJCachedPropertiesKey) return cachedPropertiesDict_;
        return nil;
    }
}

......

@end

总结

  • MJExtension使用缓存来加快转换效率,使用了两种缓存方法:第一种是使用关联对象的方法,第二种是使用静态字典的方法;
  • MJExtension使用宏定义来使得类具有归档的作用;
  • MJExtension使用属性的字符串描述(the attribute string of a property)来得到属性的类型;
  • 建议不要在多线程的使用MJExtension,避免出现问题;
  • 尽量不要过多的调用会清理缓存的方法:如+mj_setupReplacedKeyFromPropertyName:+mj_setupReplacedKeyFromPropertyName121:等。调用这些方法会清理所有类的缓存,导致转换效率降低。

你可能感兴趣的:([Edward炼金屋] MJExtension阅读笔记)