解码YYModel(二)特性

本文承接上文,解码YYModel(一)基础

在上文最后,列出一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface YYMessage : NSObject
@property (nonatomic, assign) uint64_t messageId;
@property (nonatomic, strong) NSString *content;
@property (nonatomic, strong) NSDate *time;
@property (nonatomic ,copy) NSString *name;
@end

@implementation YYMessage

+ (nullable NSArray<NSString *> *)modelPropertyBlacklist
{
return @[@"name"];
}

+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"messageId":@[@"id", @"ID", @"mes_id"],
@"time":@"t",
@"name":@"user.name"
};
}

- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
uint64_t timestamp = [dic unsignedLongLongValueForKey:@"t" default:0];
self.time = [NSDate dateWithTimeIntervalSince1970:timestamp / 1000.0];
return YES;
}
- (void)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
dic[@"t"] = @([self.time timeIntervalSince1970] * 1000).description;
}

高性能

​ 基于YYModel作者,在博文中提出的tips,理顺了下面几点性能优化的点:

缓存

​ 在下面方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary {
if (!dictionary || dictionary == (id)kCFNull) return nil;
if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;

Class cls = [self class];
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls]; //构建缓存
if (modelMeta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}

NSObject *one = [cls new];
if ([one modelSetWithDictionary:dictionary]) return one; //使用缓存
return nil;
}
  • meta Class 缓存
1
_YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
  • class info 缓存
1
YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));

Core Foundation对象

​ 在上面使用缓存的期间,均使用了CoreFoundation对象以及对应的方法。

遍历与查找

  • 查找——NSSet 代替 NSArray

    在黑白名单等地方,在查找某元素是否在集合中,采用效率更高的NSSet,而不是NSArray。因为NSSet通过遍历查找,而NSArray则是遍历查找。

    1
    2
    3
    //黑白名单超找对象时,先将黑白名单转换为NSSet
    blacklist = [NSSet setWithArray:properties];
    whitelist = [NSSet setWithArray:properties];
  • 查表

    当遇到多项选择的条件时,要尽量使用查表法实现,比如 switch/case,C Array,如果查表条件是对象,则可以用 NSDictionary 来实现。

    1
    2
    3
    4
    5
    6
    //switch/case而不是if/else
    //switch/case在库里,到处都是。

    //NSDictionary
    //在使用缓存的时候,获取对应对象的meta时,根据NSDictionary来实现
    _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
  • 减少遍历的循环次数

    在 JSON 和 Model 转换前,Model 的属性个数和 JSON 的属性个数都是已知的,这时选择数量较少的那一方进行遍历,会节省很多时间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //在设值时,如果model中_keyMappedCount的属性个数多余JSON中的属性个数,那么就以JSON属性个数遍历
    //否则,就以_keyMappedCount为范围进行遍历
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
    CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
    } else {
    CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
    CFRangeMake(0, modelMeta->_keyMappedCount),
    ModelSetWithPropertyMetaArrayFunction,
    &context);
    }
  • 遍历容器类时,选择更高效的方法

    相对于 Foundation 的方法来说,CoreFoundation 的方法有更高的性能,用 CFArrayApplyFunction() 和 CFDictionaryApplyFunction() 方法来遍历容器类能带来不少性能提升。

    1
    2
    3
    4
    5
    CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
    CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
    CFRangeMake(0, modelMeta->_keyMappedCount),
    ModelSetWithPropertyMetaArrayFunction,
    &context);

C函数与内联函数

  1. C函数,使用纯 C 函数可以避免 ObjC 的消息发送带来的开销。

  2. 内联函数

    如果 C 函数比较小,使用 inline 可以避免一部分压栈弹栈等函数调用的开销。

    在代码中,我们看到了很多函数以`static force_inline`开头,其中:
    
1
2
//参考GCC中对于强制函数执行内联的宏定义,参见下面内联函数
#define force_inline __inline__ __attribute__((always_inline))

​ 关于内联函数,多说几句:

  1. 内联函数:编译时,类似宏替换,使用函数体替换调用处的函数名。
  2. 优势:内联减少了函数调用的开销,在调用时不发生控制转移。
  3. 劣势:如果调用次数很多时,一般会增加了代码量。
  4. 写法:inline
    1. __inline是某些编译器定制的用于C代码中的inline函数,另外一些编译器还使用inline来实现类似__inline的功能。
    2. __forceinline也是编译器相关的关键字,不基于编译器的性能和优化分析而是强制代码的inline内联,即便代码膨胀严重。
    3. GCC中还定义了属性__attribute__((always_inline))来告诉编译器inline该函数。
  5. 内联函数注意点:
    1. 函数尽量短小;
    2. 尽量不要包括循环语句和开关语句;
    3. 内联函数定义要出现在第一次调用前。
  6. 与宏的异同:
    1. 编译阶段:宏定义使用预处理器preprocessor实现,只是预处理符号表的简单替换。内联函数则在编译阶段进行替换,会有类型检查和参数有效性的检测。
    2. 返回值:宏定义的返回值不能强制转换为其他类型,而内联函数可以;
    3. 安全性:内联函数具有类型检查与参数有效性的验证,而宏没有;
    4. 宏还有更多的缺点:(1)不能访问私有变量;(2)宏定义很容易产生二义性;

内存优化

​ 在 ARC 条件下,默认声明的对象是__strong 类型的,赋值时有可能会产生 retain/release 调用,如果一个变量在其生命周期内不会被释放,则使用 __unsafe_unretained 会节省很大的开销。

​ 访问具有 __weak 属性的变量时,实际上会调用 objc_loadWeak() 和 objc_storeWeak() 来完成,这也会带来很大的开销,所以要避免使用 __weak 属性。

​ 创建和使用对象时,要尽量避免对象进入 autoreleasepool,以避免额外的资源开销。

设值

  • 设值避免 KVC,在JSON转Model最后一步,设值时,下面方法大量采用setter方法:
1
2
3
4
5
6
7
static void ModelSetValueForProperty(__unsafe_unretained id model,
__unsafe_unretained id value,
__unsafe_unretained _YYModelPropertyMeta *meta) {
.......
//调用setter方法设置
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
}

​ 性能上 getter/setter要优于KVC。

  • 避免 getter/setter 调用

    如果能直接访问 ivar,则尽量使用 ivar 而不要使用 getter/setter 这样也能节省一部分开销。

黑名单/白名单

黑名单中的属性,处理过程中会忽略。

白名单外的属性,处理过程中会忽略。

黑名单,首先要实现:

1
2
3
4
+ (nullable NSArray<NSString *> *)modelPropertyBlacklist
{
return @[@"name"];
}

黑名单如何实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while (curClassInfo && curClassInfo.superCls != nil) {
// recursive parse super class, but ignore root class (NSObject/NSProxy)
// 递归解析父类,但是忽略了根类
for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
if (!propertyInfo.name) continue;
//黑名单包括该属性,继续下一个循环
if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
//白名单不包括该属性,继续下一个循环
if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
_YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
propertyInfo:propertyInfo
generic:genericMapper[propertyInfo.name]];
if (!meta || !meta->_name) continue;
if (!meta->_getter || !meta->_setter) continue;
if (allPropertyMetas[meta->_name]) continue;
allPropertyMetas[meta->_name] = meta;
}
curClassInfo = curClassInfo.superClassInfo;
}

自动类型转换

自动类型转换,其实就是将一下类型做了一定的兼容。我们可以逐步分析如何做一下兼容的。

  • NSNumber,NSURL,SEL,Class -> NSString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case YYEncodingTypeNSString:
case YYEncodingTypeNSMutableString: {
if ([value isKindOfClass:[NSString class]]) {
if (meta->_nsType == YYEncodingTypeNSString) {
} else {
}
} else if ([value isKindOfClass:[NSNumber class]]) {

} else if ([value isKindOfClass:[NSData class]]) {

} else if ([value isKindOfClass:[NSURL class]]) {

} else if ([value isKindOfClass:[NSAttributedString class]]) {
}
} break;

​ 从上面代码片段可以看出,兼容了NSString、NSNumber、NSData、NSURL、NSAttributedString。

从上面各个类转换为NSString或者NSMutableString的过程,可以参考下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//NSNumber->NSString
((NSNumber *)value).stringValue
((NSNumber *)value).stringValue.mutableCopy

//NSData->NSString
NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding];

//NSURL->NSString
((NSURL *)value).absoluteString
((NSURL *)value).absoluteString.mutableCopy

//NSAttributedString->NSString
((NSAttributedString *)value).string
((NSAttributedString *)value).string.mutableCopy

​ 其中,objc_msgSend方法的使用及原理,点击这里

  • 数字

    数字分为基础的C 基本类型,以及Foundation中的数字对象类型。

    | | C基础类型 | 对象类型 |
    | —- | ————————— | —- |
    | | bool | |
    | | int (8/16/32/64bit) | |
    | | unsigned int(8/16/32/64bit) | |
    | | float/double/long double | |

如何确定一个属性的类型呢?

_YYModelPropertyMeta中有三个类型的变量:

1
2
3
4
5
6
7
@interface _YYModelPropertyMeta : NSObject {
.......
YYEncodingType _type; ///< property's type
YYEncodingNSType _nsType; ///< property's Foundation type
BOOL _isCNumber; ///< is c number type
.......
}

​ 我们在上一篇文章中,说明了如何从property解析出_type类型,该类型指明了property的属性值以及类型。我们再重温一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//YYClassPropertyInfo.m
- (instancetype)initWithProperty:(objc_property_t)property
{
//type类型读取
type = YYEncodingGetType(attrs[i].value);
}

//从type encoding中读取出来
YYEncodingType YYEncodingGetType(const char *typeEncoding) {

switch (*type) {
.......
case 'C': return YYEncodingTypeUInt8 | qualifier;
.......
case '{': return YYEncodingTypeStruct | qualifier;
default: return YYEncodingTypeUnknown | qualifier;
}
}

​ 其中_nsType类型,是否Foundation中的对象类型:

1
2
3
4
5
6
7
8
/// Get the Foundation class type from property info.
static force_inline YYEncodingNSType YYClassGetNSType(Class cls) {
if (!cls) return YYEncodingTypeNSUnknown;
......
.........
if ([cls isSubclassOfClass:[NSSet class]]) return YYEncodingTypeNSSet;
return YYEncodingTypeNSUnknown;
}

_isCNumber类型:

1
2
3
4
5
6
7
8
9
10
11
/// Whether the type is c number.
static force_inline BOOL YYEncodingTypeIsCNumber(YYEncodingType type) {
switch (type & YYEncodingTypeMask) {
case YYEncodingTypeBool:
case YYEncodingTypeInt8:
......
case YYEncodingTypeDouble:
case YYEncodingTypeLongDouble: return YES;
default: return NO;
}
}

​ 理清楚了上面三个类型的区别,就能在JSON->Model中更好的理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//针对C 基础类型
if (meta->_isCNumber) {
NSNumber *num = YYNSNumberCreateFromID(value);
ModelSetNumberToProperty(model, num, meta);
if (num != nil) [num class]; // hold the number
} else if (meta->_nsType) {
//针对Foundation中的数字对象类型,处理很类似上面的NSString类型
case YYEncodingTypeNSValue:
case YYEncodingTypeNSNumber:
case YYEncodingTypeNSDecimalNumber: {
if (meta->_nsType == YYEncodingTypeNSNumber) {
} else if (meta->_nsType == YYEncodingTypeNSDecimalNumber) {
if ([value isKindOfClass:[NSDecimalNumber class]]) {
} else if ([value isKindOfClass:[NSNumber class]]) {
} else if ([value isKindOfClass:[NSString class]]) {
}
} else { // YYEncodingTypeNSValue
if ([value isKindOfClass:[NSValue class]]) {
}
}
} break;
}

​ 针对C 基础类型,分了两步:

  1. 从JSON中取得的value处理成NSNumber
  2. 将NSNumber赋值给Model中的property

类型安全

​ 类型安全体现在两个方面,取值与设值。继续以数字类型的解析来作分析:

  1. 从JSON中取得的value处理成NSNumber
  2. 将NSNumber赋值给Model中的property

    第1步,即取值,如何做到安全:
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
static force_inline NSNumber *YYNSNumberCreateFromID(__unsafe_unretained id value) {
static NSCharacterSet *dot;
static NSDictionary *dic;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dot = [NSCharacterSet characterSetWithRange:NSMakeRange('.', 1)];
dic = @{@"TRUE" : @(YES),
@"True" : @(YES),
@"true" : @(YES),
@"FALSE" : @(NO),
@"False" : @(NO),
@"false" : @(NO),
@"YES" : @(YES),
@"Yes" : @(YES),
@"yes" : @(YES),
@"NO" : @(NO),
@"No" : @(NO),
@"no" : @(NO),
@"NIL" : (id)kCFNull,
@"Nil" : (id)kCFNull,
@"nil" : (id)kCFNull,
@"NULL" : (id)kCFNull,
@"Null" : (id)kCFNull,
@"null" : (id)kCFNull,
@"(NULL)" : (id)kCFNull,
@"(Null)" : (id)kCFNull,
@"(null)" : (id)kCFNull,
@"<NULL>" : (id)kCFNull,
@"<Null>" : (id)kCFNull,
@"<null>" : (id)kCFNull};
});
//1. 假如value,即字典中的值,是nil或者kCFNull,那么取值为nil
if (!value || value == (id)kCFNull) return nil;
//2. 假如value是NSNumber,直接取值,不作处理
if ([value isKindOfClass:[NSNumber class]]) return value;
//3. 假如value是字符串类型,那么字符串的可能性就有很多了
if ([value isKindOfClass:[NSString class]]) {
//3-1 可能性1:若字符串中包含dic中的布尔类型与各种形式的null类型
//若是null类型,返回nil
//弱势布尔类型,将布尔型转为NSNumber返回
NSNumber *num = dic[value];
if (num != nil) {
if (num == (id)kCFNull) return nil;
return num;
}
//3-2 可能性2:里面是浮点数,包含"."
if ([(NSString *)value rangeOfCharacterFromSet:dot].location != NSNotFound) {
const char *cstring = ((NSString *)value).UTF8String;
if (!cstring) return nil;
double num = atof(cstring);
//排除非数字值isnan与无穷值isinf
if (isnan(num) || isinf(num)) return nil;
return @(num);
} else {
//3-3 可能性3:其他字符串
const char *cstring = ((NSString *)value).UTF8String;
if (!cstring) return nil;
//将字符串转为数字值,并包装成NSNumber返回
return @(atoll(cstring));
}
}
return nil;
}

​ 第2步,设值呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static force_inline void ModelSetNumberToProperty(__unsafe_unretained id model,
__unsafe_unretained NSNumber *num,
__unsafe_unretained _YYModelPropertyMeta *meta) {
switch (meta->_type & YYEncodingTypeMask) {
.......
case YYEncodingTypeFloat: {
float f = num.floatValue;
//检查是否不是数字值或者是无穷值,即无效值
if (isnan(f) || isinf(f)) f = 0;
((void (*)(id, SEL, float))(void *) objc_msgSend)((id)model, meta->_setter, f);
} break;
.......
default: break;
}
}

​ 另外,保证类型安全的更多是做了自动类型转换,保证了兼容。

系列

  1. 解码YYModel(一)基础
  2. 解码YYModel(二)特性
  3. 解码YYModel(三)参考