KVC底层原理--YYModel简述

作者: 我叫Vincent | 来源:发表于2019-07-31 11:36 被阅读267次
    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

    相关文章

      网友评论

        本文标题:KVC底层原理--YYModel简述

        本文链接:https://www.haomeiwen.com/subject/pibhkctx.html