关于KVC

356 阅读8分钟

键值编码

在iOS开发中,允许通过key名直接访问或者赋值对象的属性,而不用通过存取方法。这样就可以在运行时动态的访问和修改对象的属性,而不是在编译时确定。

定义与使用

KVC的定义是通过对NSObject的扩展来实现的,有个显示的NSKeyValueCoding类别名,对于所有继承了NSObject的类型,都能使用KVC。

寻找key

我们可以在Xcode里面,打开NSKeyValueCoding.h文件,里面有关于KVC的完整介绍。

valueForKey:

  1. 首先按照-get<Key>, -<key>, or -is<Key>的顺序来查找,如果找到的值是一个object类型就直接返回,如果是一个数据类型就会转换成NSNumber对象返回,如果是结构体类型(不仅限于:NSPoint,NSRange,NSRect,NSSize),就转换成NSValues返回
  2. 如果上面没有找到,就会开始查找下面几个方法-countOf<Key> 、 -indexIn<Key>OfObject: 、-objectIn<Key>AtIndex: 、 -<key>AtIndexes: 只要-countOf<Key>和-indexIn<Key>OfObject:和后面两个至少一个方法被发现了,就会返回一个可以相应所有NSOrderedSet方法的集合代理对象。所有的NSOrderedSet消息都会被发送到这个对象上,并且将会以组合的形式调用-countOf<Key>, -indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, and -<key>AtIndexes:这几个方法,发送到-valueForKey:上。如果对象的类实现了一个可选的函数-get<Key>:range:那么也会被调用(提高性能)。
  3. 查找-countOf<Key> and -objectIn<Key>AtIndex: (匹配NSArray类),-<key>AtIndexes:(匹配-[NSArray objectsAtIndexes:])。count方法和另外两个中的至少一个被发现,返回一个响应所有NSArray方法的集合协议对象。每一个NSArray消息都有该对象响应,并以-countOf<Key>, -objectIn<Key>AtIndex:, and -<key>AtIndexes:一些组合(组合规则如前面所说)的形式被发送到原始接受者的valueForKey上。如果这个接受者的类实现了-get<Key>:range: 那么会调用,可以有更好的性能。
  4. 继续没有找到的话,就开始匹配-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>这三个方法,如果这个三个方法都找到的话,就返回一个响应所有NSSet方法的集合协议对象。每一个NSSet消息都会发送给集合协议对象,然后该对象就会以-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>三者组合的形式响应。
  5. 还没有找到的话,就检查+accessInstanceVariablesDirectly属性,如果返回YES,就按_<key>, _is<Key>, <key>, or is<Key>查找,找到了就直接返回,一些属性会以NSNumber或NSValue 的形式返回如前面所说。
  6. 上面条件都不满足,就调用-valueForUndefinedKey:我们可以重写这个方法。默认实现是抛一个NSUndefinedKeyException异常

setValue:

setValue概括起来比较简单,分为下面三步

  1. 查找-set<Key>:方法,如果参数类型不是一个指针类型,但是接受了一个nil值,那么就会走到-setNilValueForKey: 这个方法,默认抛NSInvalidArgumentException异常,可以重写它。数值类型也会被转为相应的对象类型(NSNumber,NSValue)
  2. 没有找到的话,就会检查+accessInstanceVariablesDirectly,返回YES,就开始查找_<key>, _is<Key>, <key>, or is<Key>。如果这个变量是一个对象类型,setValue的的时候,新值会被持有,旧值会被释放。
  3. 返回NO,就触发-setValue:forUndefinedKey:方法,逻辑和上面一致

KeyPath

如果类的成员变量是另一个类或其它的复杂数据类型,就可以使用KeyPath。 有一个address类

@interface Address : NSObject

@property (nonatomic, copy) NSString *country;

@end

另一个people类

@interface People : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) KVCAddress *address;
@property (nonatomic, assign) NSInteger age;

@end

我们可以用KeyPath来设置people实例的address中的country属性

Address *address = [Address new];
address.country = @“中国”;
People *people = [People new];
people.address = address;
NSLog(@“%@|%@“, people.address.country, [people valueForKeyPath:@“address.country”]);
[people setValue:@“日本” forKeyPath:@“address.country”];
NSLog(@“%@|%@“, people.address.country, [people valueForKeyPath:@“address.country”]);

打印结果

2020-03-23 12:03:48.686338+0800 AwesomeOC[10376:165500] 中国|中国
2020-03-23 12:03:48.687086+0800 AwesomeOC[10376:165500] 日本|日本

KVC与容器类

mutableArrayValueForKey:

  1. 搜索-insertObject:in<Key>AtIndex: and -removeObjectFrom<Key>AtIndex: -insert<Key>:atIndexes: and -remove<Key>AtIndexes: 如果至少找到一个insert方法和一个remove方法,返回一个可以响应NSMutableArray所有方法代理集合,给这个代理结合发送NSMutableArray的方法,以-insertObject:in<Key>AtIndex:, -removeObjectFrom<Key>AtIndex:, -insert<Key>:atIndexes:, and -remove<Key>AtIndexes:组合的形式调用。
  2. 搜索-set<Key>: 如果找到了,每一个发送给代理集合的NSMutableArray方法都会调用-set<Key>:
  3. 没找到就检查+accessInstanceVariablesDirectly属性,返回YES,就搜索_<key> or <key>,找打了,那么发送给NSMutableArray的方法就直接给这个成员变量处理。
  4. 还是找不到,就forUndefinedKey了。

另外还有 mutableOrderedSetValueForKey,mutableSetValueForKey 关于有序容器和无序容器的方法,这里就不写了,直接在NSKeyValueCoding.h查看即可,思路都是一致的。 它们也有对应的KeyPath版本。

KVC与字典

字典使用KVC,valueForKey:内部就是返回objectForKey: 那么使用KeyPath来访问多层嵌套字典,这是一个比较方便的操作。

  • dictionaryWithValuesForKeys 提供一组key,返回这组key对应的属性,再组成一个字典
  • setValuesForKeysWithDictionary 修改model中对应的key的属性

KVC验证

用来验证key对应的value是否可用 - (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; 默认返回YES,如果重写了,就调用这个方法返回。

-(BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError {
    NSString *country = *ioValue;
    country = country.capitalizedString;
    if ([country isEqualToString:@“Japan”]) {
        return NO;
    }
    return YES;
}

Address *address = [Address new];
NSError *error;
id value = @“japan”;
NSString *key = @“country”;
BOOL result = [address validateValue:&value forKey:key error:&error];
if (result) {
    NSLog(@“键值匹配”);
    [address setValue:value forKey:key];
} else {
    NSLog(@“键值不匹配”);
}

如果我在设定某个值前需要验证一下,那么就可以重写这个方法,但是要注意,KVC在setValue时并不会主动调用这个验证函数,这里需要我们手动调用才行。

手动实现KVC

- (void)setMyValue:(id)value forKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        NSLog(@“Key不能为空”);
        return;
    }
    if ([value isKindOfClass:[NSNull class]]) {
        NSLog(@“value不能为空”);
        [self setNilValueForKey:key];
        return;
    }
    if (![value isKindOfClass:[NSObject class]]) {
        @throw @“must be a NSObject type”;
        return;
    }
    NSString *funcName = [NSString stringWithFormat:@“set%@“, key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
        [self performSelector:NSSelectorFromString(funcName) withObject:value];
        return;
    }
    unsigned int count;
    BOOL flag = false;
    Ivar *vars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar var = vars[i];
        NSString *keyName = [NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding];
        NSLog(@“keyName: %@“, keyName);
        keyName = [keyName substringFromIndex:1];
        if ([keyName isEqualToString:[NSString stringWithFormat:@“_%@“, key]]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            object_setIvar(self, var, value);
        }
    }
    if (!flag) {
        [self setValue:value forUndefinedKey:key];
    }
}

- (id)myValueForKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return nil;
    }
    NSString *funcName = [NSString stringWithFormat:@“get%@:”, key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
        return [self performSelector:NSSelectorFromString(funcName)];
    }
    
    unsigned int count;
    BOOL flag = false;
    Ivar *vars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar var = vars[i];
        NSString *keyName = [NSString stringWithFormat:@“_%@“, key];
        NSString *varName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        if ([varName isEqualToString:keyName]) {
            flag = true;
            return object_getIvar(self, var);
        }
        if ([varName isEqualToString:key]) {
            flag = true;
            return object_getIvar(self, var);
        }
    }
    if (!flag) {
        [self valueForUndefinedKey:key];
    }
    return nil;
}

KVC的使用

动态地取值、设值

访问和修改私有变量。

之前项目里面,需要给一个textView添加placeholder,有一种做法就是通过获取私有变量来实现的。但是在后来的iOS版本中,这种做法会报错,猜测是苹果在新的版本中移除了这个私有变量。

Model和字典转换

这个相信是最常见的,在获取网络数据转换model的时候经常用到

操作集合

这种方式会达到一种高阶函数的效果

NSArray *langArr = @[@“english”, @“franch”, @“chinese”];
NSArray *capLangArr = [langArr valueForKey:@“capitalizedString”];
NSLog(@“%@“, capLangArr);
NSArray *langLenArr = [langArr valueForKeyPath:@“capitalizedString.length”];
NSLog(@“%@“, langLenArr);

输出

2020-03-23 16:12:43.690950+0800 AwesomeOC[18336:330583] (
    English,
    Franch,
    Chinese
)
2020-03-23 16:12:43.691636+0800 AwesomeOC[18336:330583] (
    7,
    6,
    7
)

函数操作集合

简单集合运算符: @avg, @count, @max, @min, @sum

Book *book1 = [Book new];
book1.name = @“The Grate Gastby”;
book1.price = 22;
Book *book2 = [Book new];
book2.name = @“Time History”;
book2.price = 12;
Book *book3 = [Book new];
book3.name = @“Wrong Hole”;
book3.price = 111;
Book *book4 = [Book new];
book4.name = @“Wrong Hole”;
book4.price = 111;

NSArray *books = @[book1, book2, book3, book4];
NSNumber *sum = [books valueForKeyPath:@“@sum.price”];
NSLog(@“sum: %f”, sum.doubleValue);
NSNumber *avg = [books valueForKeyPath:@“@avg.price”];
NSLog(@“avg: %f”, avg.doubleValue);
NSNumber *count = [books valueForKeyPath:@“@count”];
NSLog(@“count: %ld”, count.integerValue);
NSNumber *min = [books valueForKeyPath:@“@min.price”];
NSLog(@“min: %f”, min.doubleValue);
NSNumber *max = [books valueForKeyPath:@“@max.price”];
NSLog(@“max: %f”, max.doubleValue);

输出

2020-03-23 16:21:35.262880+0800 AwesomeOC[18701:337719] sum: 256.000000
2020-03-23 16:21:35.263241+0800 AwesomeOC[18701:337719] avg: 64.000000
2020-03-23 16:21:35.263456+0800 AwesomeOC[18701:337719] count: 4
2020-03-23 16:21:35.263639+0800 AwesomeOC[18701:337719] min: 12.000000
2020-03-23 16:21:35.263791+0800 AwesomeOC[18701:337719] max: 111.000000

对象运算符: @distinctUnionOfObjects,@unionOfObjects

前者返回去重以后的结果,后者返回全部,这里不再细说。

总结

关于KVC的了解,其实主要是看官方文档就行,其次就是一些用法,以前对于KVC就仅仅局限在model转换这里,要么就是获取私有变量这些,这次学习下来还是很有收获的。