YYModel阅读摘要(二)特性

原文发布于: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函数与内联函数

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

  2. 内联函数

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

    ​在代码中,我们看到了很多函数以static force_inline开头,其中:

//参考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方法:
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 基础类型,分了两步:

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

类型安全

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

  1. 从JSON中取得的value处理成NSNumber
  2. 将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;
    }
}

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

系列

  1. YYModel阅读摘要(一)基础
  2. YYModel阅读摘要(二)特性
  3. YYModel阅读摘要(三)参考

你可能感兴趣的:(YYModel阅读摘要(二)特性)