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

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

作者: 洧中苇_4187 | 来源:发表于2020-05-29 16:50 被阅读0次

    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

    相关文章

      网友评论

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

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