iOS中关于KVC使用的一些小技巧

1,693 阅读4分钟

KVC是iOS开发中经常会用到的技巧, 主要包括valueForKey:/setValue:ForKey:, valueForKeyPath:/setValue:forKeyPath:两对组合方法. 最常见的理解和使用是:valueForKey:会首先查找以参数名命名的getter方法, 如果没有找到, 则在对象内寻找名称格式为_key或key的实例变量. 另外还有dictionaryWithValuesForKeys:/setValuesForKeysWithDictionary方法用于获取或设置指定的内容.

这里有两个类:

#pragma mark - Dog

@interface Dog : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *city;

@end

@implementation Dog

@end



#pragma mark - Person

@interface Person : NSObject

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

@property (nonatomic, strong) Dog *dog;

@property (nonatomic, strong) NSArray<Dog *> *dogs;

@end

@implementation Person

@end

其中, Person类有一个dogs属性. 下面将在这两个类的基础上展示一些小的demo.

基本使用

Dog *aDog = [[Dog alloc] init];
aDog.name = @"My Dog";
aDog.age = 2;
NSString *dogName = [aDog valueForKey:@"name"];
NSInteger dogAge = [[aDog valueForKey:@"age"] integerValue];
NSLog(@"dogName : %@, dogAge : %ld", dogName, dogAge);

键路径

Person *aPerson = [[Person alloc] init];
aPerson.name = @"Chris";
aPerson.age = 18;
aPerson.dog = aDog;

NSString *name = [aPerson valueForKeyPath:@"name"];
NSInteger age = [[aPerson valueForKeyPath:@"age"] integerValue];
dogName = [aPerson valueForKeyPath:@"dog.name"];
dogAge = [[aPerson valueForKeyPath:@"dog.age"] integerValue];
NSLog(@"testKeyPath: name : %@, age : %ld", name, age);
NSLog(@"testKeyPath: dogName : %@, dogAge : %ld", dogName, dogAge);

注意, 键路径的写法遵循点语法.

批量操作

使用KVC, 有时候可以非常方便地进行一些批量的操作.

Dog *dog1 = [[Dog alloc] init];
dog1.name = @"Dog 1";
dog1.age = 1;
dog1.city = @"Shanghai";

Dog *dog2 = [[Dog alloc] init];
dog2.name = @"Dog 2";
dog2.age = 2;
dog2.city = @"Shanghai";

Dog *dog3 = [[Dog alloc] init];
dog3.name = @"Dog 3";
dog3.age = 3;
dog3.city = @"Beijing";

Person *aPerson = [[Person alloc] init];
aPerson.name = @"Chris";
aPerson.age = 18;
aPerson.dogs = @[dog1, dog2, dog3];

测试代码如下:

NSArray *dogNames = [aPerson valueForKeyPath:@"dogs.name"];
NSArray *dogAges = [aPerson valueForKeyPath:@"dogs.age"];
NSMutableString *str = [[NSMutableString alloc] init];
[str appendString:@"dogNames :"];
for (NSInteger i = 0; i < dogNames.count; i++) {
    [str appendFormat:@" %@, %ld years old. ", dogNames[i], [dogAges[i] integerValue]];
}

NSArray实现valueForKeyPath:的方法是循环遍历它的内容并向每个对象发送消息.

批量修改如下:

[aPerson setValue:@"Xiamen" forKeyPath:@"dogs.city"];

cities = [aPerson valueForKeyPath:@"dogs.@distinctUnionOfObjects.city"];
NSLog(@"cities : %@", cities);

快速运算

使用KVC进行快速运算的键路径语法类似于 *** dogs.@count *** , *** dogs.@sum.age *** 等.

// dogs表示取出内容, @count即进行计算, 通知KVC机制进行键路径左侧值的对象总数
NSNumber *countOfDogs = [aPerson valueForKeyPath:@"dogs.@count"];
NSLog(@"count of dogs : %@", countOfDogs);

// 获取@sum左侧的集合, 对集合中的每个对象执行右侧操作age, 将结果组成一个集合并返回.
NSNumber *ageCountOfDogs = [aPerson valueForKeyPath:@"dogs.@sum.age"];
NSLog(@"ageCountOfDogs of dogs : %@", ageCountOfDogs);

NSNumber *ageAvgOfDogs = [aPerson valueForKeyPath:@"dogs.@avg.age"];
NSLog(@"age avg of dogs : %@", ageAvgOfDogs);
NSLog(@"age min : %@, max : %@", [aPerson valueForKeyPath:@"dogs.@min.age"], [aPerson valueForKeyPath:@"dogs.@max.age"]);

// 获取唯一值
NSArray *cities = [aPerson valueForKeyPath:@"dogs.@distinctUnionOfObjects.city"];
NSLog(@"cities : %@", cities);

这里就不作过多描述.

dictionaryWithValuesForKeys:/setValuesForKeysWithDictionary:

直接看代码更直观:

// 根据设置的key, 来进行组合结果.
Dog *lastDog = [[aPerson valueForKeyPath:@"dogs"] lastObject];
NSArray *keys = @[@"name"];
NSDictionary *values = [lastDog dictionaryWithValuesForKeys:keys];
NSLog(@"values : %@", values);

// 使用setValuesForKeysWithDictionary根据字典对Dog进行修改
NSDictionary *newValues = @{@"name": @"My Dog"};
[lastDog setValuesForKeysWithDictionary:newValues];
values = [lastDog dictionaryWithValuesForKeys:keys];
NSLog(@"values : %@", values);

总结

KVC的调用顺序

KVC的valueForkey:如何找到对应的值呢?实际上会对使用vlaueForKey方法的实例对象尝试调用如下方法,有返回值即停止并返回:

-get<Key>, -<key>, -is<Key>等getter方法,
-countOf<Key, -objectIn<Key>AtIndex:
_<key>, _is<Key>, <key>, is<Key>

对于结果,如果是引用对象,直接返回。如果是标量,使用NSNumber包装并返回。否则,使用NSValue包装并返回。 所以,使用KVC来操作实例对象的属性和实例变量是非常容易的事。即,KVO与runtime(能够查询到实例对象的所有实例变量和属性等)结合起来,可以做到对任意属性和实例变量进行查询和修改,尽管一些属性和实例变量并未暴露出来。

其实,默认情况下,KVC相关的方法有很多,其中有一些并不是我们熟知的。

-valueForKey:
-setValue:forKey:
-validateValue:forKey:error:
-mutableArrayValueForKey:
-mutableOrderedSetValueForKey:
-mutableSetValueForKey:

-valueForKeyPath:
-setValue:forKeyPath:
-validateValue:forKeyPath:error:
-mutableArrayValueForKeyPath:
-mutableOrderedSetValueForKeyPath:
-mutableSetValueForKeyPath:

-valueForUndefinedKey:
-setValue:forUndefinedKey:
-setNilValueForKey:
-dictionaryWithValuesForKeys:
-setValuesForKeysWithDictionary:

集合

NSArray

-valueForKey:方法会遍历每个元素,执行-valueForKey:,并且结果组合成一个新的数组并返回。对于返回nil的情况,会使用NSNull来代替。

Return an array containing the results of invoking -valueForKey: on each of the receiver's elements. The returned array will contain NSNull elements for each instance of -valueForKey: returning nil.

-setValue:forKey:则会遍历每个元素,执行-setValue:forKey:方法。

NSSet

-valueForKey:方法会遍历每个元素,执行-valueForKey:,并且结果组合成一个新的set并返回。 注意,对于-valueForKey:的结果为nil的元素,不会将其结果nil存入返回的set中。这一点与NSArray不同。

Return a set containing the results of invoking -valueForKey: on each of the receiver's members. The returned set might not have the same number of members as the receiver. The returned set will not contain any elements corresponding to instances of -valueForKey: returning nil (in contrast with -[NSArray(NSKeyValueCoding) valueForKey:], which may put NSNulls in the arrays it returns).

NSDictionary

这个没什么可说的,分别对应于NSDictionary的objectForKey:和setObject:forKey:方法。

Demo

Demo请参考:

DemoKVC 需要注意的是:

不要滥用KVC, KVC需要解析字符串来计算需要的结果, 因此速度较慢. 且无法进行错误检查.

最后补充一个KVC在AFNetworking中的使用案例

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

KVC如果使用得当,确实能够在代码中起到画龙点睛的作用。