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全是字典类型,
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
在一些极端的精度转换场景,也没有很好的解决这个问题,
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
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可以调用一个方法处理归解档,黑白名单),同时提供了很多简洁方便的接口,使用的时候,调用的代码特别的简单.
关于阅读源码从哪里着手,这里分享一下我的习惯
- 先写个Demo,造几条数据,跑起来,看看网上别人分析的成果,作为参考.
首先你得用起来,看看它的使用难易程度,造一组数据,打断点,跑起来,一步步跟进去,看它走了哪些方法,这个阶段相当于是在熟悉API,过了一遍整个代码的结构走向,这个阶段大致占用整个阅读源码时间的10%左右.
- 过一遍包含的文件,从结构简单的着手分析.
先把代码量少的文件在自己的Demo里跟着敲一遍,从造轮子的角度,跟着作者实现一遍,把自己的知识盲区现场找资料,记录下来,从小到大,从易到难,把所有的文件实现一遍,标记好自己不会的,或者没有领悟到的知识点(我会在自己实现的代码里用 "???" 标记自己不理解的地方,想知道的时候就全局搜),把好的设计模式记录下来,把自己的理解注释,放到方法的附近,深入进去体会整个流程,这个阶段大致占用整个阅读源码时间的70%左右.
3.对比源码,复盘一下整个逻辑,再跑一遍你的数据,看有没有什么问题.
用 beyong Compare 先和源码对比,相信我,即使你跟着敲,代码也可能出错,对于里面逻辑理解不到位的,再加深印象,把自己实现的代码保存好,可以最快速度能够打开的地方(我是传到GitHub),方便我们在其他地方遇到在框架中类似的知识点,能够随时记录,解决.这个阶段大致占用整个阅读源码时间的15%左右.
- 总结框架的优缺点,你认为还有没有值得改进的地方.
不要怀疑自己,优秀的框架,不一定没有BUG,如果你觉得有不妥的地方,你先试着改改,看看效果,随时有时间,回来翻翻自己敲的代码,梳理脉络,同时对自己不理解的地方加深印象,这是一个最漫长的过程,也是沉淀的过程,这个阶段大致占用整个阅读源码时间的5%左右.
小结:
本文从阅读的层面,大致梳理了一下MJExtension的逻辑,三天时间跟着敲了一遍,看到吐,也体会了造轮子的不易,和需要长时间的知识积累,在这里对那些无私提供开源代码的同仁表示尊敬...
阅读源码是个痛苦且享受的过程---如果有下一篇,我会从代码设计的角度来聊聊这个框架...下个框架阅读是----->MJRefreshing
这里是跟着敲的一份代码,里面添加了注释,个人的理解,方便后来者.
--->MJExtensionCopy
网友评论