开源项目-阅读MJExtension,你能学到什么(附注释Dem

2020-05-29  本文已影响0人  洧中苇_4187

1. 设置关联对象用来缓存MJProperty

/// -property关联---MJProperty,缓存起来
/// @param property 需要关联的属性
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    **根据key取出上次缓存过的关联对象, self是目标对象
    **property 是需要关联的key - 这里使用模型的某个属性地址作为key
    **一般来说,我们用static const 修饰的字符串作为key,
    **关于key ,也有方法名 _cmd, 只要保证唯一性即可,通过@(property_getName(property))可以打印其属性名
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;

        **设置关联对象 
        **self--目标对象
        **property --关联的key
        **propertyObj--关联的对象 ,这里可以把key 和 被关联对象的关系理解为字典的key-value
        **如果将propertyObj传入nil,则表示清除这个关联

        **关联策略 ---objc_AssociationPolicy
        **OBJC_ASSOCIATION_ASSIGN ---assign-关联对象弱引用
        **OBJC_ASSOCIATION_RETAIN_NONATOMIC --- retain-nonatomic-非原子性-强引用
        **OBJC_ASSOCIATION_COPY_NONATOMIC --- copy-nonatomic-复制-非原子性
        **OBJC_ASSOCIATION_RETAIN --- retain-atomic-强引用-原子性
        **OBJC_ASSOCIATION_COPY ---copy--atomic-复制-原子性

        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}

2. property_getAttributes()使用 关于更详细的可查看官方文档
文档位置 :官方文档

- (void)setProperty:(objc_property_t)property
{
    _property = property;
    
    MJExtensionAssertParamNotNil(property);
    
    // 1.属性名
    _name = @(property_getName(property));
    
    // 2.成员类型
    **可以使用property_getAttributes函数发现属性的名称、@encode类型字符串和属性的其他属性。
    **T@"NSString",&,N,V_name
    NSString *attrs = @(property_getAttributes(property));
    NSUInteger dotLoc = [attrs rangeOfString:@","].location;
    NSString *code = nil;
    NSUInteger loc = 1;
    if (dotLoc == NSNotFound) { // 没有,
        code = [attrs substringFromIndex:loc];
    } else {
        **@"NSString"
        code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)];
    }
    **把name的属性@"NSString"缓存起来
    _type = [MJPropertyType cachedTypeWithCode:code];
}

举例:

@property (nonatomic,strong)NSString *name; 类型:T@"NSString",&,N,V_name
@property (nonatomic,assign)int64_t age; 类型:Tq,N,V_age
@property (nonatomic,assign)char *nickName; 类型:T*,N,V_nickName
@property (nonatomic,assign)BOOL isMarried; 类型:TB,N,V_isMarried
具体格式参考: image.png

3. 一些编译宏的说明

- (id)valueForObject:(id)object
{
    if (self.type.KVCDisabled) return [NSNull null];
    
    id value = [object valueForKey:self.name];
    
    // 32位BOOL类型转换json后成Int类型
    /** https://github.com/CoderMJLee/MJExtension/issues/545 */
    // 32 bit device OR 32 bit Simulator
#if defined(__arm__) || (TARGET_OS_SIMULATOR && !__LP64__)
    **__arm__  32位真机
    **TARGET_OS_SIMULATOR 模拟器
    **!__LP64__  非64位,
    **这里展开说一下,如果在64位机器上,int是32位,long是64位,pointer也是64位,那我们就称该机器是LP64的,也称I32LP64,类似的还有LLP64,ILP64,SILP64...
    **这里是因为在32位环境下,bool值会转换成int类型,所以这里如果是bool类型,把value强转为BOOL类型
    if (self.type.isBoolType) {
        value = @([(NSNumber *)value boolValue]);
    }
#endif
    
    return value;
}

4. 关于字典转模型中-模型中套着模型 和 多级映射的一些说明

模型中套着模型 这里有两个类-结构如下:
@interface MJPerson : NSObject
@property (nonatomic,strong)NSString *name;
@property (nonatomic,strong)NSString *sex;
@property (nonatomic,strong)NSString *girlsType;
@property (nonatomic,strong)NSString *characters;
@property (nonatomic,strong)NSString *testNMB;
@property (nonatomic,strong)NSString *hobits;
@property (nonatomic,strong)NSArray <MJStudent *>*studentsArray;

@end


@interface MJStudent : NSObject
@property (copy, nonatomic) NSString *image;
@property (copy, nonatomic) NSString *url;
@property (copy, nonatomic) NSString *name;

@end

    NSDictionary *dic = @{@"name":@"西西里的美丽传说",
                          @"sex":@"女性",
                          @"girlsType":@"cute",
                          @"characters":@"lovely",
                          @"testId":@"XXXXXXXXXXXXXX",
                          @"hobits":@{
                                  @"piano":@"good",
                                  @"writing":@"exllent",
                                  @"professional":@"pick-up-artist"
                          },
                          @"studentsArray":@[
                                  @{
                                      @"image":@"student_image.png",
                                      @"url":@"https://www.baidu.com",
                                      @"name":@"Mickey",
                                  },
                                  @{
                                      @"image":@"pretty_hot.png",
                                      @"url":@"https://www.google.com",
                                      @"name":@"Jefrrey",
                                  },
                          ]
    };

4.1> 像上面这种, studentsArray-@[MJStudent,...] ,数组中包含模型的情况,MJExtension是怎样处理的呢???
首先你得在MJPerson.m中声明 数组 和模型的关系,如下:

+ (NSDictionary *)mj_objectClassInArray{
    return @{
        @"studentsArray":@"MJStudent"
    };
}

每个MJPerson里面的属性都会调用上述这个方法,返回为空,就会直接处理该属性,如果有映射关系,则会返回对应的类,对应到代码里 对于加解锁MJ_LOCK不太清楚的小伙伴可以读我的上一篇 关于信号量 dispatch_semaphore .

"模型包含模型-根据多级映射的key取值 - propertyName:属性名"
Class clazz = [self mj_objectClassInArray][propertyName];

"最后会保存到这个字典里 - @{@"MJPerson" : @"MJStudent"}"
MJ_LOCK(self.objectClassInArrayLock);
Class objectClass = self.objectClassInArrayDict[key];
MJ_UNLOCK(self.objectClassInArrayLock);

这么保存起来有什么用呢??? 
这里是通过类名(MJPerson) 取出来的类名(MJStudent),objectClass就是MJStudent ,这里就可以复用正常字典转模型的步骤了
value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];

4.2> 还有一种 就是多级映射key的情况 什么意思???
在字典转模型前,添加如下代码,则name属性赋值studentsArray数组里面name的值(Jefrrey), MJPerson的hobits 属性 会赋值 professional对应的值(pick-up-artist).

 [MJPerson mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
        return @{@"name" : @"studentsArray[1].name",
                 @"hobits":@"hobits.professional"
        };
    }];

它是怎么做到的,核心代码就是在下面这个方法,

- (NSArray *)propertyKeysWithStringKey:(NSString *)stringKey{
    if (stringKey.length == 0) return nil;
    
    NSMutableArray *propertyKeys = [NSMutableArray array];
    // 如果有多级映射
    NSArray *oldKeys = [stringKey componentsSeparatedByString:@"."];
    
    for (NSString *oldKey in oldKeys) {
        NSUInteger start = [oldKey rangeOfString:@"["].location;
        if (start != NSNotFound) { // 有索引的key
            NSString *prefixKey = [oldKey substringToIndex:start];
            NSString *indexKey = prefixKey;
            if (prefixKey.length) {
                MJPropertyKey *propertyKey = [[MJPropertyKey alloc] init];
                propertyKey.name = prefixKey;
                [propertyKeys addObject:propertyKey];
                
                indexKey = [oldKey stringByReplacingOccurrencesOfString:prefixKey withString:@""];
            }
            
            /** 解析索引 **/
            // 元素
            NSArray *cmps = [[indexKey stringByReplacingOccurrencesOfString:@"[" withString:@""] componentsSeparatedByString:@"]"];//取出数组中的元素
            for (NSInteger i = 0; i<cmps.count - 1; i++) {
                MJPropertyKey *subPropertyKey = [[MJPropertyKey alloc] init];
                subPropertyKey.type = MJPropertyKeyTypeArray;
                subPropertyKey.name = cmps[I];
                [propertyKeys addObject:subPropertyKey];
            }
        } else { // 没有索引的key
            MJPropertyKey *propertyKey = [[MJPropertyKey alloc] init];
            propertyKey.name = oldKey;
            [propertyKeys addObject:propertyKey];
        }
    }
    
    return propertyKeys;
}

这里MJExtension设计了一个MJPropertyKey的东西,这个属性 能指明属性的映射关系.
4.2.1 如果属性是一一对应的 比如@"sex":@"女性",那么MJPropertyKey.sex = 女性,返回的数组只包含这个MJPropertyKey,而且type为空,
4.2.2 如果对应多级映射的key 比如studentsArray[1].name
那么它会生成多个数组,读者注意它的name 和 type类型,映射层级越深,它返回的数组长度就越长;
字典的映射类似,返回的MJPropertyType全是字典类型,

image.png

5. 关于字典 和 常量字符串的初始化


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';

@implementation NSObject (Property)

5.1> 字符串用做绑定字典的取值,都初始化'\0' 这样取值的时候不会出问题吗??? 当然不会,它使用的时候是取的地址,不是用的值,赋值 '\0'能保证只占用一个字节,你看看打印的地址值就知道,而且每个地址值之间相隔一个字节.

(lldb) p &MJReplacedKeyFromPropertyNameKey
(const char *) $12 = 0x00000001041fdd8a <no value available>
(lldb) p &MJReplacedKeyFromPropertyName121Key
(const char *) $13 = 0x00000001041fdd8b <no value available>
(lldb) p &MJNewValueFromOldValueKey
(const char *) $14 = 0x00000001041fdd8c <no value available>
(lldb) p &MJObjectClassInArrayKey
(const char *) $15 = 0x00000001041fdd8d <no value available>
(lldb) p &MJCachedPropertiesKey
(const char *) $16 = 0x00000001041fdd8e <no value available>
(lldb) 

5.2> 根据不同的key的地址,注意接收的是一个指针地址,取值的时候也是对比的地址值,返回不同的字典,且全部字典只初始化一次,如果你的工程中大量用到字典或者数组的情况,也可以采取类似方法初始化

+ (NSMutableDictionary *)mj_propertyDictForKey:(const void *)key
{
    static NSMutableDictionary *replacedKeyFromPropertyNameDict;
    static NSMutableDictionary *replacedKeyFromPropertyName121Dict;
    static NSMutableDictionary *newValueFromOldValueDict;
    static NSMutableDictionary *objectClassInArrayDict;
    static NSMutableDictionary *cachedPropertiesDict;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        replacedKeyFromPropertyNameDict = [NSMutableDictionary dictionary];
        replacedKeyFromPropertyName121Dict = [NSMutableDictionary dictionary];
        newValueFromOldValueDict = [NSMutableDictionary dictionary];
        objectClassInArrayDict = [NSMutableDictionary dictionary];
        cachedPropertiesDict = [NSMutableDictionary dictionary];
    });
    
    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;
}

6. 关于一些精度要求的属性赋值问题 和 精度计算类NSDecimalNumber
在某些精度要求很高的使用场景中,必须使用像NSDecimalNumber这种类来提高计算精度,下面场景中得知,用double计算的精度是比较差的;同时超高精度的计算是非常消耗性能的,所以还是根据使用场景,合理选择.


6.1>小知识点:数组可以添加 [NSNull null],它是个对象,所以有别于nil
NSMutableArray *array = [NSMutableArray arrayWithCapacity:5];
    for (int64_t i = 0; i<5; i++) {
        [array addObject:[NSNull null]];
    }
NSLog(@"%@",array);

打印结果:
2020-05-29 09:25:11.315419+0800 testProj[30796:3289459] (
    "<null>",
    "<null>",
    "<null>",
    "<null>",
    "<null>"
)

6.2> 过期方法警告消除

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
xxxxx 这里调用过期方法
#pragma clang diagnostic pop

6.3> 在只需要判断包不包含某个元素时,用NSMutableSet比NSMutableArray效率要高

+ (BOOL)isFromNSObjectProtocolProperty:(NSString *)propertyName
{
    if (!propertyName) return NO;
    
    static NSSet<NSString *> *objectProtocolPropertyNames;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        unsigned int count = 0;
        objc_property_t *propertyList = protocol_copyPropertyList(@protocol(NSObject), &count);
        NSMutableSet *propertyNames = [NSMutableSet setWithCapacity:count];
        for (int i = 0; i < count; i++) {
            objc_property_t property = propertyList[I];
            NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            if (propertyName) {
                [propertyNames addObject:propertyName];
            }
        }
        objectProtocolPropertyNames = [propertyNames copy];
        free(propertyList);
    });
    
    return [objectProtocolPropertyNames containsObject:propertyName];
}

似乎MJEXtension在一些极端的精度转换场景,也没有很好的解决这个问题,

image.png

7. 下面来大致梳理一下MJExtension 字典转模型的整个逻辑
7.1> 查看有没有缓存这个类的属性白名单-黑名单,如果没有,创建黑白名单字典

    static NSMutableDictionary *allowedPropertyNamesDict; 属性白名单
    static NSMutableDictionary *ignoredPropertyNamesDict; 属性黑名单
    static NSMutableDictionary *allowedCodingPropertyNamesDict; 归档白名单
    static NSMutableDictionary *ignoredCodingPropertyNamesDict; 归档黑名单

7.2> 通过runtime,遍历模型类的所有需要转换的属性,为每个属性创建一个
MJProperty的对象:

@interface MJProperty : NSObject
/** 成员属性 */
@property (nonatomic, assign) objc_property_t property;
/** 成员属性的名字 */
@property (nonatomic, readonly) NSString *name;
/** 成员属性的类型 */
@property (nonatomic, readonly) MJPropertyType *type;
/** 成员属性来源于哪个类(可能是父类) */
@property (nonatomic, assign) Class srcClass;
@end

MJProperty 里面又包含了一个MJPropertyType:

@interface MJPropertyType : NSObject
/** 类型标识符 */
@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;

通过上述MJProperty类型记录编码的信息,并缓存起来,缓存的手段是采取关联对象的方式

NSString *attrs = @(property_getAttributes(property));
获得编码类型的好处就是,知道要将你转换成哪种类型,
比如你需要转换的字典里 有 这个 @"isMarried":@"false",
那么MJExtension会根据 isMarried 是BOOL类型,把false转换成 @NO

缓存方式:self ,唯一的key,缓存的value ,缓存策略
objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

7.3> 接着就会拿到一个装满MJProperty 的数组,拿到数组后开始遍历,并为每个属性赋值

MJProperty 数组
NSArray *cachedProperties = [self mj_properties];

7.3.1> 查看是否有属性黑名单,或者白名单不包含的内容,这些属性直接跳过,不用赋值,

拿到属性名,检查是否外免实现了新旧值替换的方法,如果有就替换,没有,继续下一步
id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];

7.3.2> 查看是否有某个属性数组对应模型属性的情况,studentsArray - MJStudent

递归调用,若里面还是数组就继续遍历,直到里面是字典为止
+ (NSMutableArray *)mj_objectArrayWithKeyValuesArray:(id)keyValuesArray context:(NSManagedObjectContext *)context

7.3.3> 如果是基本数据类型,就需要注意精度问题,尽量用DecimalNumber转化,避免丢失精度
7.3.4> 经过转换后, 最终检查 value 与 property 是否匹配,最终给成员属性赋值,这里就走完了全程

- (void)setValue:(id)value forObject:(id)object
{
    if (self.type.KVCDisabled || value == nil) return;
    [object setValue:value forKey:self.name];KVC赋值
}

8. 有疑问的地方:
8.1 > 当你的属性中有个字符串的指针变量时,字典转模型时会控制台出现报错的情况,看作者的实现:MJ有定义MJPropertyTypePointer属性,但是没有对它进行处理(当然我这种情况是比较极端,一般不会用这种作为属性)

@interface MJPerson : NSObject
@property (nonatomic)char *nickName;
@end

控制台打印(报不支持KVC的错)
2020-05-29 15:23:06.866380+0800 MJExtensionTest[38048:3457126] 
[<MJPerson 0x600003e40930> setValue:forUndefinedKey:]: 
this class is not key value coding-compliant for the key nickName.

8.2 > MJExtension里面有一个方法,省去一些无关代码,可以看到传进来的变量名(propertyName) 和里面使用的变量名一样,这样不会有问题吗???

+ (BOOL)isFromNSObjectProtocolProperty:(NSString *)propertyName
{
    dispatch_once(&onceToken, ^{
            NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        }
    });
    return [objectProtocolPropertyNames containsObject:propertyName];
}

于是我做了个验证,在dispatch外部的打印没有疑问,但是在dispatch方法里面,它怎么知道我想访问的是外部的arg,还是局部变量 arg,我理解可能是编译器从小范围优先查找的,如果读者有更好的解释, let me know

image.png

8.3> 关于一些小的点 ~~~

作者有个手误 应该是setProperty
- (void)setPorpertyKeys:(NSArray *)propertyKeys forClass:(Class)c 
这里做了两次判断,中间没干什么事儿,应该是作者手误 image.png
在MJExtension提了上述说的两个小问题,得到了解决,这是结果 image.png

MJExtension 有哪些值得我们学习的地方???
1.> 声明 和 使用 key的方式 static const char MJReplacedKeyFromPropertyNameKey = '\0';,比较地址,用最少的内存达到了同样的效果
2.> 缓存结果的方式,设置关联对象,存取都很方便
3.> 使用block的方式,放到函数里面遍历,写法更简洁,
4.> 分类的使用,使得方法调用简便.
5.> 单例中创建字典,绑定key,写法简洁,一目了然
6.> 属性,归档 黑白名单的设计,能够全局管理属性.
7.> 遍历前,对值的提前判断-过滤,很大程度上提高了性能
8.> 代码精炼,很多方法都调用同一个方法,能够批量处理多种情况(多个block可以调用一个方法处理归解档,黑白名单),同时提供了很多简洁方便的接口,使用的时候,调用的代码特别的简单.

关于阅读源码从哪里着手,这里分享一下我的习惯

  1. 先写个Demo,造几条数据,跑起来,看看网上别人分析的成果,作为参考.
    首先你得用起来,看看它的使用难易程度,造一组数据,打断点,跑起来,一步步跟进去,看它走了哪些方法,这个阶段相当于是在熟悉API,过了一遍整个代码的结构走向,这个阶段大致占用整个阅读源码时间的10%左右.
  1. 过一遍包含的文件,从结构简单的着手分析.
    先把代码量少的文件在自己的Demo里跟着敲一遍,从造轮子的角度,跟着作者实现一遍,把自己的知识盲区现场找资料,记录下来,从小到大,从易到难,把所有的文件实现一遍,标记好自己不会的,或者没有领悟到的知识点(我会在自己实现的代码里用 "???" 标记自己不理解的地方,想知道的时候就全局搜),把好的设计模式记录下来,把自己的理解注释,放到方法的附近,深入进去体会整个流程,这个阶段大致占用整个阅读源码时间的70%左右.

3.对比源码,复盘一下整个逻辑,再跑一遍你的数据,看有没有什么问题.
用 beyong Compare 先和源码对比,相信我,即使你跟着敲,代码也可能出错,对于里面逻辑理解不到位的,再加深印象,把自己实现的代码保存好,可以最快速度能够打开的地方(我是传到GitHub),方便我们在其他地方遇到在框架中类似的知识点,能够随时记录,解决.这个阶段大致占用整个阅读源码时间的15%左右.

  1. 总结框架的优缺点,你认为还有没有值得改进的地方.
    不要怀疑自己,优秀的框架,不一定没有BUG,如果你觉得有不妥的地方,你先试着改改,看看效果,随时有时间,回来翻翻自己敲的代码,梳理脉络,同时对自己不理解的地方加深印象,这是一个最漫长的过程,也是沉淀的过程,这个阶段大致占用整个阅读源码时间的5%左右.

小结:
本文从阅读的层面,大致梳理了一下MJExtension的逻辑,三天时间跟着敲了一遍,看到吐,也体会了造轮子的不易,和需要长时间的知识积累,在这里对那些无私提供开源代码的同仁表示尊敬...

阅读源码是个痛苦且享受的过程---如果有下一篇,我会从代码设计的角度来聊聊这个框架...下个框架阅读是----->MJRefreshing

这里是跟着敲的一份代码,里面添加了注释,个人的理解,方便后来者.
--->MJExtensionCopy

上一篇下一篇

猜你喜欢

热点阅读