83

KVC底层原理--YYModel简述 - 简书

 5 years ago
source link: https://www.jianshu.com/p/cce3a7b99c84?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

KVC底层原理--YYModel简述

0.2612019.07.31 11:36:53字数 1,886阅读 790
webp
KVC底层原理

YYModel的作用就是字典转模型,在了解YYModel前,我们先了解下KVC的知识。

KVC:也称之键值编码,是一种采用了NSKeyValueCoding协议的对象(直接或间接继承NSObject时会为基本方法提供默认实现)通过间接访问其属性的机制,也就是符合键值编码,对象可通过字符串参数来简单而统一的消息对其属性进行寻址,例如valueForKey:setValue:forKey:。在一些特殊情况下,KVC还可以简化代码。官方文档直通车

KVC底层原理

举例:用键值编码实现数据源方法

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}

或者我们常用的设置textFiled的placeholder颜色

[self.textFiled setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];

当调用协议的 getter(比如valueForKey:), 默认实现根据Accessor Search Patterns中描述的规则确定为指定键提供值的特定访问器方法或实例变量. 如果返回值不是对象,getter会使用这个值初始一个NSNumber对象或NSValue(对于结构体)对象替代。同样setter(比如setValue:forKey:)通过特定键或访问方法或实例变量时确定的数据类型,如果数据类型不是对象,setter首先会向传入的值对象发送适当的 <type>Value 消息(intValue)提取基础数据并存储该数据。仅限Objective-C,因为Swift的所有属性都是对象。

自动包装和解包不仅限于 NSPoint,NSRange,NSRect,和 NSSize. 也可以是 NSValue 对象。举例:

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end

使用KVC获取myClass的threeFloats:默认调用threeFloats的getter,然后将返回值包装在NSValue中返回。

NSValue* result = [myClass valueForKey:@"threeFloats"];

当然,我们也可以使用KVC来设置threeFloats的值

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
KVC取值和赋值的过程

首先,我们先根据官方文档找到Accessor Search Patterns来查看根据输入key的搜索流程。下面是官方文档的部分说明,具体参考官方文档

说的就是在getter的时候,先按照get<Key>, <key>, is<Key>, or _<key>的顺序来查找,找到了就直接调用,然后判断收到的属性值是一个对象指针直接返回,该值是NSNumber支持的标量类型则用包装NSNumber返回,否则包装成 NSValue 对象返回。如果没有找到则在实例中搜索countOf<Key>, objectIn<Key>AtIndex:(对应于NSArray基本方法)和<key>AtIndexes:(对应于NSArray的objectsAtIndexes),如果找到则创建一个响应所有NSArray方法的集合代理对象并返回,如果还是没有找到则查找countOf<Key>, enumeratorOf<Key>和memberOf<Key>:(对应于 NSSet 类的基本方法),如果三个方法都找到,会创建一个响应所有NSSet方法的集合代理对象并返回该方法。如果还没找到并且接收者对象的accessInstanceVariablesDirectly(是否开启间接访问)返回 YES,则会顺序查找_<key>, _is<key>, <key>, is<key>,如果找到了则返回相应的值(对象指针、NSNumber或者NSValue),如果最后还是没找到则会执行valueForUndefindKey:,默认抛出异常,NSObject子类可以重写。

在setter的时候也是顺序查找访问方法名set<key>, _set<key>,找到了就则使用输入值执行,没找到且accessInstanceVariablesDirectly返回YES,则按照顺序查找_<key>, _is<key>, <key>, is<key>,如果找到了就执行没找到则执行setValue:forUndefinedKey:抛出未定义key的异常,同样NSObject子类可以重写。

当没有对异常进行处理的话,不出意外会崩溃。

在赋值和取值过程中,当value为空的时候,会执行setNilValueForKey,如果Key值不存在则执行setValue:forUndefinedKey
KVC也可以用于验证key或key-path的方法,我们也可以为属性提供验证方法。在响应validateValue:forKey:error:方法的时候,会查找valudate<Key>:error:是否实现,如果实现了则根据实现方法的自定义逻辑返回YES或者NO,如果没实现则系统默认返回YES,NSError用来返回error信息。

KVC异常处理

那在实际运用的时候万一出现意外,要怎么规避呢?可以在NSObject的分类做下相应的处理(OC的对象几乎都可以追溯到NSObject)。

// .h文件代码
- (void)k_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)k_valueForKey:(NSString *)key;
// ------------------华丽的分割线----------------------
// .m文件代码
- (void)k_setValue:(id)value forKey:(NSString *)key {
    NSError  *error;
    BOOL validate = [self validateValue:&value forKey:key error:&error];
    NSArray *arr = [self getIvarListName];
    if (validate) {
        if ([arr containsObject:key]) {
            [self setValue:value forKey:key];
        }else{
            NSLog(@"%@ 不存在变量",key);
        }
    }
}

- (nullable id)k_valueForKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        NSLog(@"key为nil或者空值");
        return nil;
    }
    NSArray *arr = [self getIvarListName];
    if ([arr containsObject:key]) {
        return [self valueForKey:key];
    }
    NSLog(@"%@ 不存在变量",key);
    return nil;
}

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    if (*ioValue == nil || inKey == nil || inKey.length == 0) {
        NSLog(@"value 可能为nil  或者key为nil或者空值");
        return NO;
    }
    return YES;
}

- (NSMutableArray *)getIvarListName {
    NSMutableArray *mulArr = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mulArr addObject:ivarName];
    }
    free(ivars);
    return mulArr;
}

记得添加#import <objc/runtime.h>,在调用k_setValue forKey或者k_valueForKey的时候会自动检测value和key是否有效值,如果不是有效值会抛出

KVC使用
  • 字典的使用
NSDictionary* dict = @{
                           @"oneString":@"one",
                           @"num":@123,
                           @"list":@[@1, @2, @3]
                           };
    Person *p = [[Person alloc] init];
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"%@--%d---%@",p.oneString, p.num, p.list);
    // 键数组转模型到字典
    NSArray *array = @[@"oneString",@"num"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);

打印结果如下:

one--123---(
    1,
    2,
    3
)

{
    num = 123;
    oneString = one;
}
  • KVC消息传递
    NSArray *array = @[@"One",@"Two",@"Three",@"HH"];
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"%@",lenStr);// 消息从array传递给了string
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"%@",lowStr);
    // 也支持聚合操作符
    int count = [[array valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"%d", count);

打印结果:

(
    3,
    3,
    5,
    2
)

(
    one,
    two,
    three,
    hh
)

 4
  • 补充
    当然了,聚合操作符还有很多,列举一些可能用到的:
    @avg:通过right-key-path读取集合中每个元素读取属性,并转换为double类型(nil用0代替),计算他们的平均值,然后返回NSNumber。
    @ sum:原理同 '@avg',返回所有元素的和。
    @ count:以NSNumber实例返回集合中所有对象的数量,如果有right-key-path忽略。
    @ max:通过right-key-path在集合中查找并返回最大的元素。查找会使用由Foundation类(例如 NSNumber) 定义的compare: 方法,所以right-key-path指定的属性必须是一个对这个消息有意义的响应,查找忽略集合中的nil值。
    @min:原理同 '@max',返回最小的元素。
    下面的也可以对集合进行操作
    @distinctUnionOfObjects:返回对right-key-path指定属性进行合并操作后的去重数组。
    @ unionOfObjects:和distinctUnionOfObjects行为相似, 但是不会删除重复对象。
    @ distinctUnionOfSets:和distinctUnionOfObjects行为相似获取交集。

键值模型--YYModel

字典转模型的大概流程是分析对象,获取到对象的所有ivar,并将ivar一一赋值。

首先,我们进入YYModel的字典转模型入口yy_modelWithDictionarymodelWithJSON方法,部分代码展示:

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    
    Class cls = [self class];
    // 分析cls
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    
    NSObject *one = [cls new];
    if ([one yy_modelSetWithDictionary:dictionary]) return one;
    return nil;
}

metaWithClass:方法内部先用Core Foundation的CFMutableDictionaryRef进行缓存已经处理过的key-value,以备下次直接使用,并用dispatch_semaphore_t来确保字典的读取安全。如果没有该缓存或者不需要更新则进行创建_YYModelMeta对象,_YYModelMeta首先对黑名单和白名单进行处理,再次判断modelContainerPropertyGenericClass是否需要对特殊属性进行替换(按照不同的类型进行遍历 ),然后开始遍历自己和父类所有的属性和方法,生成与数据源相对应的字典映射(具体可网上查找),接着处理_YYModelPropertyMeta,判断是NSString、NSArray,一切准备好后开始执行yy_modelSetWithDictionary:方法响应CFArrayApplyFunction,再次执行ModelSetWithPropertyMetaArrayFunction,获取到的value传入ModelSetValueForProperty中,通过objc_msgSend发送meta->_setter来完成属性值的设置。

switch (meta->_nsType) {
                case YYEncodingTypeNSString:
                case YYEncodingTypeNSMutableString: {
                    if ([value isKindOfClass:[NSString class]]) {
                        if (meta->_nsType == YYEncodingTypeNSString) {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
                        } else {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
                        }
                    } else if ([value isKindOfClass:[NSNumber class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSNumber *)value).stringValue :
                                                                       ((NSNumber *)value).stringValue.mutableCopy);
                    } else if ([value isKindOfClass:[NSData class]]) {
                        NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding];
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, string);
                    } else if ([value isKindOfClass:[NSURL class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSURL *)value).absoluteString :
                                                                       ((NSURL *)value).absoluteString.mutableCopy);
                    } else if ([value isKindOfClass:[NSAttributedString class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSAttributedString *)value).string :
                                                                       ((NSAttributedString *)value).string.mutableCopy);
                    }
                } break;

该文章为记录本人的学习路程,希望能够帮助大家,知识共享,共同成长,共同进步!!!文章地址:https://www.jianshu.com/p/cce3a7b99c84


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK