原文发布于:wenghengcong.com
本文承接上文,YYModel阅读摘要(一)基础。
在上文最后,列出一个实例:
@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 *)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,理顺了下面几点性能优化的点:
缓存
在下面方法中
+ (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 缓存
_YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
- class info 缓存
YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
Core Foundation对象
在上面使用缓存的期间,均使用了CoreFoundation对象以及对应的方法。
遍历与查找
-
查找——NSSet 代替 NSArray
在黑白名单等地方,在查找某元素是否在集合中,采用效率更高的
NSSet
,而不是NSArray
。因为NSSet
通过遍历查找,而NSArray
则是遍历查找。//黑白名单超找对象时,先将黑白名单转换为NSSet blacklist = [NSSet setWithArray:properties]; whitelist = [NSSet setWithArray:properties];
-
查表
当遇到多项选择的条件时,要尽量使用查表法实现,比如 switch/case,C Array,如果查表条件是对象,则可以用 NSDictionary 来实现。
//switch/case而不是if/else //switch/case在库里,到处都是。 //NSDictionary //在使用缓存的时候,获取对应对象的meta时,根据NSDictionary来实现 _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
-
减少遍历的循环次数
在 JSON 和 Model 转换前,Model 的属性个数和 JSON 的属性个数都是已知的,这时选择数量较少的那一方进行遍历,会节省很多时间。
//在设值时,如果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() 方法来遍历容器类能带来不少性能提升。
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas, CFRangeMake(0, modelMeta->_keyMappedCount), ModelSetWithPropertyMetaArrayFunction, &context);
C函数与内联函数
C函数,使用纯 C 函数可以避免 ObjC 的消息发送带来的开销。
-
内联函数
如果 C 函数比较小,使用 inline 可以避免一部分压栈弹栈等函数调用的开销。
在代码中,我们看到了很多函数以
static force_inline
开头,其中:
//参考GCC中对于强制函数执行内联的宏定义,参见下面内联函数
#define force_inline __inline__ __attribute__((always_inline))
关于内联函数,多说几句:
- 内联函数:编译时,类似宏替换,使用函数体替换调用处的函数名。
- 优势:内联减少了函数调用的开销,在调用时不发生控制转移。
- 劣势:如果调用次数很多时,一般会增加了代码量。
- 写法:
inline
__inline
是某些编译器定制的用于C代码中的inline函数,另外一些编译器还使用inline来实现类似__inline的功能。- __forceinline也是编译器相关的关键字,不基于编译器的性能和优化分析而是强制代码的inline内联,即便代码膨胀严重。
- GCC中还定义了属性_attribute_((always_inline))来告诉编译器inline该函数。
- 内联函数注意点:
- 函数尽量短小;
- 尽量不要包括循环语句和开关语句;
- 内联函数定义要出现在第一次调用前。
- 与宏的异同:
- 编译阶段:宏定义使用预处理器preprocessor实现,只是预处理符号表的简单替换。内联函数则在编译阶段进行替换,会有类型检查和参数有效性的检测。
- 返回值:宏定义的返回值不能强制转换为其他类型,而内联函数可以;
- 安全性:内联函数具有类型检查与参数有效性的验证,而宏没有;
- 宏还有更多的缺点:(1)不能访问私有变量;(2)宏定义很容易产生二义性;
内存优化
在 ARC 条件下,默认声明的对象是__strong 类型的,赋值时有可能会产生 retain/release 调用,如果一个变量在其生命周期内不会被释放,则使用 __unsafe_unretained 会节省很大的开销。
访问具有 __weak 属性的变量时,实际上会调用 objc_loadWeak() 和 objc_storeWeak() 来完成,这也会带来很大的开销,所以要避免使用 __weak 属性。
创建和使用对象时,要尽量避免对象进入 autoreleasepool,以避免额外的资源开销。
设值
- 设值避免 KVC,在JSON转Model最后一步,设值时,下面方法大量采用setter方法:
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 这样也能节省一部分开销。
黑名单/白名单
黑名单中的属性,处理过程中会忽略。
白名单外的属性,处理过程中会忽略。
黑名单,首先要实现:
+ (nullable NSArray *)modelPropertyBlacklist
{
return @[@"name"];
}
黑名单如何实现:
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
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的过程,可以参考下:
//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
中有三个类型的变量:
@interface _YYModelPropertyMeta : NSObject {
.......
YYEncodingType _type; ///< property's type
YYEncodingNSType _nsType; ///< property's Foundation type
BOOL _isCNumber; ///< is c number type
.......
}
我们在上一篇文章中,说明了如何从property解析出_type
类型,该类型指明了property的属性值以及类型。我们再重温一下:
//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中的对象类型:
/// 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
类型:
/// 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中更好的理解:
//针对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 基础类型,分了两步:
- 从JSON中取得的value处理成NSNumber
- 将NSNumber赋值给Model中的property
类型安全
类型安全体现在两个方面,取值与设值。继续以数字类型的解析来作分析:
- 从JSON中取得的value处理成NSNumber
- 将NSNumber赋值给Model中的property
第1步,即取值,如何做到安全:
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,
@"" : (id)kCFNull,
@"" : (id)kCFNull,
@"" : (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步,设值呢?
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;
}
}
另外,保证类型安全的更多是做了自动类型转换,保证了兼容。
系列
- YYModel阅读摘要(一)基础
- YYModel阅读摘要(二)特性
- YYModel阅读摘要(三)参考