iOS KVC 实现探究

KVC

我们都知道iOS开发中有很多黑魔法,KVC就是其中之一,在平时开发中我们也会使用KVC去获取一些系统未公开的API方法,但同时我们可能要承担一些被拒或者因系统API改变导致的问题。这篇文章我们从源码的角度分析KVC的实现。

KVC使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)kvcValueForKeyTest {
self.p = [Person new];

self.p.name = @"LeeWong";
self.p.age = 30;

NSLog(@"Person age = %@",self.p.name);
NSLog(@"Person name = %@",@(self.p.age));
}

- (void)kvcSetValueForKey {
self.p = [Person new];

[self.p setValue:@"LeeWong" forKey:@"name"];
[self.p setValue:@(30) forKey:@"age"];

NSLog(@"Person age = %@",[self.p valueForKey:@"age"]);
NSLog(@"Person name = %@",[self.p valueForKey:@"name"]);
}

打印结果:

1
2
3
4
5
2020-12-19 18:52:54.058580+0800 Runtime-KVC[99046:16759483] Person age = 30
2020-12-19 18:52:54.058700+0800 Runtime-KVC[99046:16759483] Person name = LeeWong
2020-12-19 18:52:54.058784+0800 Runtime-KVC[99046:16759483] -----------------------------
2020-12-19 18:52:54.058882+0800 Runtime-KVC[99046:16759483] Person age = LeeWong
2020-12-19 18:52:54.058990+0800 Runtime-KVC[99046:16759483] Person name = 30

这两种情况是我们平时应用最广泛的KVC(Key-Value-Coding)的方法,通常用来获取属性值或者给对象的某个属性值赋值,即使对象的属性是私有的只要我们可以确认他的key值,我们就可以获取或者设置值。

KVC原理

在查看KVC相关方法时,我们发现其主要的方法都集中在NSKeyValueCoding.h这个文件中,下面我们来看下这个文件中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

@end

@interface NSArray<ObjectType>(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
- (nullable ObjectType)valueForKey:(NSString *)key;

@end

@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)

- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

@end

@interface NSOrderedSet<ObjectType>(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

- (void)setValue:(nullable id)value forKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

@end

@interface NSSet<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;

- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

这个文件中主要包含了
NSObject(NSKeyValueCoding),
NSArray<ObjectType>(NSKeyValueCoding),
NSDictionary<KeyType,ObjectType>(NSKeyValueCoding),
NSMutableDictionary<KeyType,ObjectType>(NSKeyValueCoding),
NSOrderedSet<ObjectType>(NSKeyValueCoding),
NSSet<ObjectType>(NSKeyValueCoding)
主要是常用的NSObject以及集合类型NSArray,NSDictionary,NSMutableDictionary,NSOrderedSet,NSSet的KVC相关方法分类。从这个分类中的分组我们也将本篇文章要讲述的内容分为两部分NSOject和集合类型的属性。

KVC实现猜想

我们在前面一篇文章中提到了KVO的实现,系统实际上继承我们要监听的类创建了一个子类,然后在子类中重写对应属性的setter方法,然后在设置值方法的前后调用了willChangeValueForKeydidChangeValueForKey的方法通知外部的监听者。

那么我们首先看下KVC获取到的对象的类型有没有改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)kvoObjectClassChangeTest {
self.father = [Father new];

Son *son1 = [Son new];
Son *son2 = [Son new];
self.father.name = @"LeeWong";
self.father.children = @[son1,son2];

NSLog(@"Father 的children 类型是%@",[[self.father valueForKey:@"children"] class]);
NSLog(@"Father 的children 类型是%@",object_getClass([self.father valueForKey:@"children"]));

NSLog(@"Father 的name 类型是 %@",[[self.father valueForKey:@"name"] class]);
NSLog(@"Father 的name 类型是 %@",object_getClass([self.father valueForKey:@"name"]));

}

打印结果:

1
2
3
4
2020-12-19 20:40:29.671973+0800 Runtime-KVC[1432:16837560] Father 的children 类型是__NSArrayI
2020-12-19 20:40:29.672078+0800 Runtime-KVC[1432:16837560] Father 的children 类型是__NSArrayI
2020-12-19 20:40:29.672189+0800 Runtime-KVC[1432:16837560] Father 的name 类型是 __NSCFConstantString
2020-12-19 20:40:29.672306+0800 Runtime-KVC[1432:16837560] Father 的name 类型是 __NSCFConstantString

这里我们看出KVC并没有给我们要获取的属性或者类动态添加子类或者其他属性,那么到底是怎么实现的呢?

否定了刚才的想法后,我们知道Runtime是可以动态获取到对象的所有属性的,那是否意味着当我们去给对象的属性通过setValue:forKey方法设值的时候是动态的获取这个对象的属性列表,如果属性列表中包含要设置的key那么久调用这个key对应的设置方法实现的呢?

下面我们通过这段代码验证下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@synthesize name = _name;


- (void)setName:(NSString *)name {
_name = name;
NSLog(@"Person setName %@",name);
}

- (NSString *)name {
NSLog(@"Person getname %@",_name);
return _name;
}

- (void)kvoSetMethodCallTest {
self.father = [Father new];
[self.father setValue:@"LeeWong" forKey:@"name"];

NSLog(@"kvoSetMethodCallTest father name %@",self.father.name);
}

我们调用kvoSetMethodCallTest方法后控制台输出结果如下:

1
2
3
2020-12-19 20:53:05.265737+0800 Runtime-KVC[1701:16847901] Person setName LeeWong
2020-12-19 20:53:05.265856+0800 Runtime-KVC[1701:16847901] Person getname LeeWong
2020-12-19 20:53:05.265937+0800 Runtime-KVC[1701:16847901] kvoSetMethodCallTest father name LeeWong

通过上述输出我们看到实际上无论是setValue:forKey还是valueForKey:最终都是调用到了属性的setter和getter方法。

那么我们的猜想基本得到验证了,那么我们下面就来看下是如何调用到getter和setter方法的呢?

KVC的实现

在了解KVC实现之前我们首先需要了解下KVC相关方法:

setValue: forKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

/* Given a value and a key that identifies an attribute, set the value of the attribute. Given an object and a key that identifies a to-one relationship, relate the object to the receiver, unrelating the previously related object if there was one. Given a collection object and a key that identifies a to-many relationship, relate the objects contained in the collection to the receiver, unrelating previously related objects if there were any.

The default implementation of this method does the following:
1. Searches the class of the receiver for an accessor method whose name
matches the pattern -set<Key>:. If such a method is found the type of its
parameter is checked. If the parameter type is not an object pointer type but the
value is nil -setNilValueForKey: is invoked. The default implementation of
-setNilValueForKey: raises an NSInvalidArgumentException, but you can override
it in your application. Otherwise, if the type of the method's parameter is an
object pointer type the method is simply invoked with the value as the argument.
If the type of the method's parameter is some other type the inverse of the
NSNumber/NSValue conversion done by -valueForKey: is performed before the
method is invoked.

2. Otherwise (no accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the
receiver for an instance variable whose name matches the pattern _<key>,
_is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found
and its type is an object pointer type the value is retained and the result is
set in the instance variable, after the instance variable's old value is first
released. If the instance variable's type is some other type its value is set
after the same sort of conversion from NSNumber or NSValue as in step 1.

3. Otherwise (no accessor method or instance variable is found), invokes -setValue:forUndefinedKey:. The default implementation of
-setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override
it in your application.


Compatibility notes:
- For backward binary compatibility with -takeValue:forKey:'s behavior, a method whose name matches the pattern -_set<Key>: is also recognized in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.
- For backward binary compatibility, -unableToSetNilForKey: will be invoked instead of -setNilValueForKey: in step 1, if the implementation of -unableToSetNilForKey: in the receiver's class is not NSObject's.
- The behavior described in step 2 is different from -takeValue:forKey:'s, in which the instance variable search order is <key>, _<key>.
- For backward binary compatibility with -takeValue:forKey:'s behavior, -handleTakeValue:forUnboundKey: will be invoked instead of -setValue:forUndefinedKey: in step 3, if the implementation of -handleTakeValue:forUnboundKey: in the receiver's class is not NSObject's.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key;

实际上上面的方法注释中已经明确告知我们方法的实现了:

  • 给定一个值和一个标识属性的键,请设置属性的值;
  • 给定一个对象和一个标识一对一关系的key,将该对象与接收者相关联,如果存在则取消先前相关的对象的关联;
  • 给定一个集合对象和一个标识多对多关系的key,将包含在集合中的对象与接收者相关联,如果存在则取消先前相关的对象的关联;

方法的实现步骤如下:

  • 搜索该类以及父类中名称符合-set:格式的方法,如果找到,检查其参数类型。如果参数类型不是对象类型且参数为nil,则调用-setNilValueForKey:方法,这个方法的默认实现是抛出NSInvalidArgumentException异常,不过你可以重写该方法自行实现。如果参数类型为对象类型,set:方法会被直接调用,这个参数也会被直接使用。如果参数能被转化为NSNumber/NSValue类型,参数会在存取器方法被调用之前进行转换。
  • 如果方法没有被找到,如果对象的+accessInstanceVariablesDirectly属性返回的是YES,那么按照_, _is, , is的顺序搜索该类的实例变量。如果找到这个实例变量,当其为对象类型时,该实例变量会在旧值释放之后被设置新值。当其为其他类型时,那么按照步骤1中的类型转换规则设置这个实例变量的值。
  • 如果上述两步都失败了,方法和实例变量都没有被找到,-setValue:forUndefinedKey:方法将会被调用。这个方法的默认实现是抛出NSUndefinedKeyException异常,不过你可以重写该方法自行实现。

下面我们来验证下API所说:

属性设置为nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Father.m

- (void)setNilValueForKey:(NSString *)key {
NSLog(@"Father setNilValueForKey %@",key);
}

ViewController.m

- (void)setValueForKeyTest {
self.father = [Father new];
[self.father setValue:nil forKey:@"name"];
[self.father setValue:nil forKey:@"age"];
}

控制台输出结果为:

1
2
2020-12-19 21:42:29.523439+0800 Runtime-KVC[2388:16881195] Person setName (null)
2020-12-19 21:42:29.523581+0800 Runtime-KVC[2388:16881195] Father setNilValueForKey age

这验证了第一步中说的查找set方法,如果找到了name且未对象类型那么直接调用setName:方法,value会被直接使用,如果找到了setAge方法,但参数不是对象类型且值为nil,那么直接调用setNilValueForKey方法(我们实现了这个方法在内部做了输出操作)。

查找对应的成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface Person ()
{
NSString *_isName1;
NSInteger _isAge1;

}

+ (BOOL)accessInstanceVariablesDirectly {
NSLog(@"Father accessInstanceVariablesDirectly");
return YES;
}

- (NSString *)getisNameValue {
return _isName1;
}

- (NSInteger)getisAgeValue {
return _isAge1;
}

ViewController.m
- (void)setValueForKeyTest {
self.father = [Father new];
[self.father setValue:@"LeeWong" forKey:@"name1"];
NSLog(@"setValueForKeyTest getisNameValue %@",[self.father getisNameValue]);

[self.father setValue:@(10) forKey:@"age1"];
NSLog(@"setValueForKeyTest getisAgeValue %@",@([self.father getisAgeValue]));
}

控制台打印结果为:

1
2
3
4
2020-12-19 21:54:26.320358+0800 Runtime-KVC[2685:16893209] Father accessInstanceVariablesDirectly
2020-12-19 21:54:26.320474+0800 Runtime-KVC[2685:16893209] setValueForKeyTest getisNameValue LeeWong
2020-12-19 21:54:26.320589+0800 Runtime-KVC[2685:16893209] Father accessInstanceVariablesDirectly
2020-12-19 21:54:26.320705+0800 Runtime-KVC[2685:16893209] setValueForKeyTest getisAgeValue 10

这个结果验证了我们setValue:forKey:的的第二步,搜索当前类中是否存在_<key>, _is<Key>, <key>, or is<Key>(示例中我们是存在_age1/_name1)的实例变量。

未找到对应的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
Person.m

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"Person setValue %@ forUndefinedKey %@",value,key);
}

ViewController.m
- (void)setValueForUndefineKeyTest {
self.father = [Father new];
[self.father setValue:@"LeeWong" forKey:@"name1"];

[self.father setValue:@(10) forKey:@"age1"];
}

控制台打印结果:

1
2
2020-12-19 22:01:02.601408+0800 Runtime-KVC[2814:16898946] Person setValue LeeWong forUndefinedKey name1
2020-12-19 22:01:02.601546+0800 Runtime-KVC[2814:16898946] Person setValue 10 forUndefinedKey age1

综合上面几步,我们总结了下setValue:forKey:的调用流程为:

调用流程

valueForKey:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* Given a key that identifies an attribute or to-one relationship, return the 
attribute value or the related object. Given a key that identifies a to-many
relationship, return an immutable array or an immutable set that contains all of
the related objects.

The default implementation of this method does the following:
1.Searches the class of the receiver for an accessor method whose name matches
the pattern -get<Key>, -<key>, or -is<Key>, in that order. If such a method
is found it is invoked. If the type of the method's result is an object
pointer type the result is simply returned. If the type of the result is one
of the scalar types supported by NSNumber conversion is done and an NSNumber
is returned. Otherwise, conversion is done and an NSValue is returned (new in Mac OS 10.5: results of arbitrary type are converted to NSValues, not just NSPoint, NRange, NSRect, and NSSize).

2 (introduced in Mac OS 10.7). Otherwise (no simple accessor method is found),
searches the class of the receiver for methods whose names match the patterns
-countOf<Key> and -indexIn<Key>OfObject: and -objectIn<Key>AtIndex:
(corresponding to the primitive methods defined by the NSOrderedSet class) and
also -<key>AtIndexes: (corresponding to -[NSOrderedSet objectsAtIndexes:]). If
a count method and an indexOf method and at least one of the other two
possible methods are found, a collection proxy object that responds to all
NSOrderedSet methods is returned. Each NSOrderedSet message sent to the
collection proxy object will result in some combination of -countOf<Key>,
-indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, and -<key>AtIndexes: messages
being sent to the original receiver of -valueForKey:. If the class of the
receiver also implements an optional method whose name matches the pattern
-get<Key>:range: that method will be used when appropriate for best
performance.

3 Otherwise (no simple accessor method or set of ordered set access methods
is found), searches the class of the receiver for methods whose names match
the patterns -countOf<Key> and -objectIn<Key>AtIndex: (corresponding to the
primitive methods defined by the NSArray class) and (introduced in Mac OS
10.4) also -<key>AtIndexes: (corresponding to -[NSArray objectsAtIndexes:]).
If a count method and at least one of the other two possible methods are
found, a collection proxy object that responds to all NSArray methods is
returned. Each NSArray message sent to the collection proxy object will result
in some combination of -countOf<Key>, -objectIn<Key>AtIndex:, and
-<key>AtIndexes: messages being sent to the original receiver of
-valueForKey:. If the class of the receiver also implements an optional method
whose name matches the pattern -get<Key>:range: that method will be used when
appropriate for best performance.

4 (introduced in Mac OS 10.4). Otherwise (no simple accessor method or set of
ordered set or array access methods is found), searches the class of the
receiver for a threesome of methods whose names match the patterns
-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>: (corresponding to the
primitive methods defined by the NSSet class). If all three such methods are
found a collection proxy object that responds to all NSSet methods is
returned. Each NSSet message sent to the collection proxy object will result
in some combination of -countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>:
messages being sent to the original receiver of -valueForKey:.

5 Otherwise (no simple accessor method or set of collection access methods is
found), if the receiver's class' +accessInstanceVariablesDirectly property
returns YES, searches the class of the receiver for an instance variable whose
name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order.
If such an instance variable is found, the value of the instance variable in
the receiver is returned, with the same sort of conversion to NSNumber or
NSValue as in step 1.

6 Otherwise (no simple accessor method, set of collection access methods, or
instance variable is found), invokes -valueForUndefinedKey: and returns the
result. The default implementation of -valueForUndefinedKey: raises an
NSUndefinedKeyException, but you can override it in your application.

Compatibility notes:
- For backward binary compatibility, an accessor method whose name matches the pattern -_get<Key>, or -_<key> is searched for between steps 1 and 3. If such a method is found it is invoked, with the same sort of conversion to NSNumber or NSValue as in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.
- The behavior described in step 5 is a change from Mac OS 10.2, in which the instance variable search order was <key>, _<key>.
- For backward binary compatibility, -handleQueryWithUnboundKey: will be invoked instead of -valueForUndefinedKey: in step 6, if the implementation of -handleQueryWithUnboundKey: in the receiver's class is not NSObject's.
*/
- (nullable id)valueForKey:(NSString *)key;

这个方法的默认实现如下:

  • 按照-get, -, -is的顺序搜索该类的获取方法,若找到,则直接调用。如果方法调用的结果是id类型,直接把结果返回。如果方法调用的结果是能够被NSNumber转换的标量类型,则结果会被转为NSNumber返回。否则对于一般的标量类型,这些类型将会被转化为NSValue(在Mac OS 10.5及以后,不仅仅支持NSPoint, NRange, NSRect,以及NSSize这些类型
  • 搜索该类的-countOf, -indexInOfObject:方法,还有-objectInAtIndex:(对应被NSOrderedSet类所定义的方法),-AtIndexes:(对应-[NSOrderedSet objectsAtIndexes:])方法。如果-countOf, -indexInOfObject:这两个方法被找到,另外两个方法中的至少一个被找到,那么这个能响应NSOrderedSet所有方法的集合代理对象会被返回。发送给原来消息接收者的-valueForKey:消息,将会被这个集合代理对象的-countOf, -indexInOfObject:, -objectInAtIndex:, -AtIndexes:这些方法共同处理。如果这个代理对象也实现了可选的-get:range:方法,这将有助于增强性能。
  • 搜索该类的-countOf方法,还有-objectInAtIndex:(对应被NSArray类所定义的方法),-AtIndexes:(对应-[NSArray objectsAtIndexes:])方法。如果-countOf这个方法被找到,另外两个方法中的至少一个被找到,那么这个能响应NSArray所有方法的集合代理对象会被返回。发送给原来消息接收者的-valueForKey:消息,将会被这个集合代理对象的-countOf, -objectInAtIndex:, -AtIndexes:这些方法共同处理。如果这个代理对象也实现了可选的-get:range:方法,这将有助于增强性能
  • 搜索-countOf, -enumeratorOf, -memberOf:这些被NSSet类所定义的方法。如果这三个方法都能被找到,那么这个能响应NSSet所有方法的集合代理对象会被返回。发送给原来消息接收者的-valueForKey:消息,将会被这个集合代理对象的countOf, -enumeratorOf, -memberOf:这些方法共同处理。
  • 如果存取器方法、ordered set、array以及set的代理方法都没有被找到,倘若此时消息接收者的+accessInstanceVariablesDirectly属性返回的是YES(默认实现就是返回YES),那么按照_, _is, , is的顺序搜索该类的实例变量。如果找到这个实例变量,那么按照步骤1中的类型转换规则返回这个实例变量的值。
  • 调用-valueForUndefinedKey:方法并返回结果。这个方法的默认实现是抛出NSUndefinedKeyException异常,不过你可以重写该方法自行实现。

下面我们来验证下上述步骤:

查找key对应的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ViewController.m
- (void)valueForKeyTestStep1 {
self.son = [Son new];
id result = [self.son valueForKey:@"school"];
NSLog(@"valueForKeyTestStep1 result %@ result class %@",result,[result class]);
}

Son.m
#pragma mark - valueForKey
// 优先级由高到低
- (NSString *)getSchool {
return @"Beijiing";
}

- (NSString *)school {
return @"Shanghai";
}

- (NSString *)isSchool {
return @"HongKong";
}

控制台输出

1
2020-12-26 20:45:27.422284+0800 Runtime-KVC[47753:19135113] valueForKeyTestStep1 result HongKong result class __NSCFConstantString

我们可以通过依次注释上述三个方法,控制台输出分别为:BeijiingShanghaiHongKong,如果三个方法都注释了,那么程序会因为找不到key对应的value而crash

1
2
3
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Son 0x600003707240> valueForUndefinedKey:]: this class is not key value coding-compliant for the key school.'
terminating with uncaught exception of type NSException
搜索集合类型相关方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

ViewController.m

- (void)valueForKeyTestStep2 {
self.son = [Son new];
id result = [self.son valueForKey:@"school"];
NSLog(@"valueForKeyTestStep2 result %@ result class %@",result,[result class]);
}

Son.m

// school key 对应的值有几个
- (NSInteger)countOfSchool {
NSLog(@"countOfSchool");
return 2;
}

// school对应index是多少
- (NSInteger)indexInSchoolOfObject:(NSString *)school {
NSLog(@"indexInSchoolOfObject %@",school);
return 1;
}

// index 下标对应的值是多少
- (NSString *)objectInSchoolAtIndex:(NSInteger)index {
NSLog(@"objectInSchoolAtIndex %@",@(index));
if (index % 2 == 0) {
return @"Beijing";
} else {
return @"HongKong";
}
}

控制台输出:

1
2
3
4
5
6
7
8
12-26 20:58:29.557431+0800 Runtime-KVC[48005:19146834] countOfSchool
2020-12-26 20:58:29.557542+0800 Runtime-KVC[48005:19146834] countOfSchool
2020-12-26 20:58:29.557652+0800 Runtime-KVC[48005:19146834] objectInSchoolAtIndex 0
2020-12-26 20:58:29.557739+0800 Runtime-KVC[48005:19146834] objectInSchoolAtIndex 1
2020-12-26 20:58:29.557856+0800 Runtime-KVC[48005:19146834] valueForKeyTestStep2 result {(
Beijing,
HongKong
)} result class NSKeyValueOrderedSet

从上面的打印结果来看,实际上并非如API所说:indexInSchoolOfObject为必须实现,因为我们在调用valueForKey时并未调用到该方法。数据结果也验证了countOfSchool表示key对应的集合value有几个值,每个index对应的值为什么。从最终打印结果看valueForKey最终返回的是NSKeyValueOrderedSet

NSArray 搜索数组相关方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ViewController.m 

- (void)valueForKeyTestStep3 {
self.son = [Son new];
id result = [self.son valueForKey:@"school"];
NSLog(@"valueForKeyTestStep3 result %@ result class %@",result,[result class]);
}

Son.m

- (NSInteger)countOfSchool {
return 3;
}

- (NSString *)objectInSchoolAtIndex:(NSInteger)index {
if (index % 2 == 0) {
return @"Beijing";
} else {
return @"HongKong";
}
}

控制台输出结果:

1
2
3
4
5
6
7
8
9
10
2020-12-26 21:00:54.850506+0800 Runtime-KVC[48058:19149212] countOfSchool
2020-12-26 21:00:54.850608+0800 Runtime-KVC[48058:19149212] countOfSchool
2020-12-26 21:00:54.850710+0800 Runtime-KVC[48058:19149212] objectInSchoolAtIndex 0
2020-12-26 21:00:54.850815+0800 Runtime-KVC[48058:19149212] objectInSchoolAtIndex 1
2020-12-26 21:00:54.850920+0800 Runtime-KVC[48058:19149212] objectInSchoolAtIndex 2
2020-12-26 21:00:54.851031+0800 Runtime-KVC[48058:19149212] valueForKeyTestStep3 result (
Beijing,
HongKong,
Beijing
) result class NSKeyValueArray

与上面NSKeyValueOrderedSet集合类似,本次搜索方法返回的结果为NSKeyValueArray即数组类型,countOfSchool表示返回的数组中有几个元素,objectInSchoolAtIndex表示返回的数组每个元素是什么,valueForKey最终返回的是数组和数组中的每一个元素。

NSSet 相关搜索方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ViewController.m

- (void)valueForKeyTestStep4 {
self.son = [Son new];
id result = [self.son valueForKey:@"school"];
NSLog(@"valueForKeyTestStep4 result %@ result class %@",result,[result class]);
}

Son.m
- (NSInteger)countOfSchool {
NSLog(@"countOfSchool");
return 2;
}

- (NSEnumerator<id> *)enumeratorOfSchool {
NSLog(@"objectInSchoolAtIndex ");
NSSet *schools = [NSSet setWithArray:@[@"Beijing",@"Hongkong"]];
return [schools objectEnumerator];
}

- (BOOL)memberOfSchool:(NSString *)school {
NSLog(@"memberOfSchool %@",school);
return YES;
}

控制台输出:

1
2
3
4
5
6
7
2020-12-26 21:24:41.942115+0800 Runtime-KVC[48572:19174700] countOfSchool
2020-12-26 21:24:41.942285+0800 Runtime-KVC[48572:19174700] countOfSchool
2020-12-26 21:24:41.942370+0800 Runtime-KVC[48572:19174700] objectInSchoolAtIndex
2020-12-26 21:24:41.942503+0800 Runtime-KVC[48572:19174700] valueForKeyTestStep4 result {(
Beijing,
Hongkong
)} result class NSKeyValueSet

countOfSchoolenumeratorOfSchool两个方法比较好理解,表示school的个数和school集合的遍历器。memberOfSchool实际并未清楚到底是什么意思,这里有两种猜想

  • 是否是某个类型
  • 是否是集合中的对象

当我们去掉这个方法的实现时,会直接崩溃

1
2
3
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Son 0x60000298fdc0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key school.'
terminating with uncaught exception of type NSException

当我们将这个方法的实现改为:

1
2
3
4
- (void)memberOfSchool:(NSString *)school {
NSLog(@"memberOfSchool %@",school);
// return YES;
}

依然得到与上述相同的结果,因此这里无法判断这个方法的实现应该是什么样的

搜索实例变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ViewController.m

- (void)valueForKeyTestStep5 {
self.son = [Son new];
id result = [self.son valueForKey:@"school"];
NSLog(@"valueForKeyTestStep5 result %@ result class %@",result,[result class]);
}

Son.m

- (instancetype)init {
if (self = [super init]) {
_isSchool = @"Beijing";
}
return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}

控制台输出:

1
2020-12-26 21:29:52.568972+0800 Runtime-KVC[48691:19180589] valueForKeyTestStep5 result Beijing result class __NSCFConstantString

这里我们定义了一个实例变量_isSchool,valueForKey方法在搜索集合相关属性无果后,会查找是否存在对应名称的实例变量,如果找到对应的实例变量,则返回。

valueForUndefinedKey
1
2
3
4
5
6
7
8
9
10
- (void)valueForKeyTestStep6 {
self.son = [Son new];
id result = [self.son valueForKey:@"school"];
NSLog(@"valueForKeyTestStep6 result %@ result class %@",result,[result class]);
}

- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"valueForUndefinedKey %@",key);
return @"Beijing";
}

控制台输出:

1
2
2020-12-26 21:35:16.596774+0800 Runtime-KVC[48788:19185406] valueForUndefinedKey school
2020-12-26 21:35:20.486420+0800 Runtime-KVC[48788:19185406] valueForKeyTestStep6 result Beijing result class __NSCFConstantString

当上述5步均无法找到对应方法,那么系统最终会调用valueForUndefinedKey方法,这个方法的默认实现就是上面我们贴的崩溃,不过我们可以重写这个方法,并在这个方法中记录对应的key值并返回一个默认值。

综上我们可以总结valueForKey方法调用的流程实际上是一个方法查找的流程,具体如下图

集合类型

KVC除了上述对于所有对象都适用的设置方法setValue:forKey:和获取方法valueForKey:外还有专门对于集合类型对象的方法

  • mutableArrayValueForKey:
  • mutableOrderedSetValueForKey:
  • mutableSetValueForKey:

这里我们拿mutableArrayValueForKey方法举例说明

同样我们先来看下方法的API说明

mutableArrayValueForKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* Given a key that identifies an _ordered_ to-many relationship, return a mutable 
array that provides read-write access to the related objects. Objects added to the
mutable array will become related to the receiver, and objects removed from the
mutable array will become unrelated.

The default implementation of this method recognizes the same simple accessor
methods and array accessor methods as -valueForKey:'s, and follows the same direct
instance variable access policies, but always returns a mutable collection proxy
object instead of the immutable collection that -valueForKey: would return. It
also:

1 Searches the class of the receiver for methods whose names match the
patterns -insertObject:in<Key>AtIndex: and -removeObjectFrom<Key>AtIndex:
(corresponding to the two most primitive methods defined by the NSMutableArray
class), and (introduced in Mac OS 10.4) also -insert<Key>:atIndexes: and
-remove<Key>AtIndexes: (corresponding to -[NSMutableArray
insertObjects:atIndexes:] and -[NSMutableArray removeObjectsAtIndexes:).
If at least one insertion method and at least one removal method are found
each NSMutableArray message sent to the collection proxy object will result
in some combination of -insertObject:in<Key>AtIndex:,
-removeObjectFrom<Key>AtIndex:, -insert<Key>:atIndexes:, and
-remove<Key>AtIndexes: messages being sent to the original receiver of
-mutableArrayValueForKey:. If the class of the receiver also implements an
optional method whose name matches the pattern
-replaceObjectIn<Key>AtIndex:withObject: or (introduced in Mac OS 10.4)
-replace<Key>AtIndexes:with<Key>: that method will be used when appropriate
for best performance.

2 Otherwise (no set of array mutation methods is found), searches the class of
the receiver for an accessor method whose name matches the pattern -set<Key>:.
If such a method is found each NSMutableArray message sent to the collection
proxy object will result in a -set<Key>: message being sent to the original
receiver of -mutableArrayValueForKey:.

3 Otherwise (no set of array mutation methods or simple accessor method is
found), if the receiver's class' +accessInstanceVariablesDirectly property
returns YES, searches the class of the receiver for an instance variable whose
name matches the pattern _<key> or <key>, in that order. If such an instance
variable is found, each NSMutableArray message sent to the collection proxy
object will be forwarded to the instance variable's value, which therefore
must typically be an instance of NSMutableArray or a subclass of
NSMutableArray.

4 Otherwise (no set of array mutation methods, simple accessor method, or
instance variable is found), returns a mutable collection proxy object anyway.
Each NSMutableArray message sent to the collection proxy object will result in
a -setValue:forUndefinedKey: message being sent to the original receiver of
-mutableArrayValueForKey:. The default implementation of
-setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can
override it in your application.

Performance note: the repetitive -set<Key>: messages implied by step 2's description are a potential performance problem. For better performance implement insertion and removal methods that fulfill the requirements for step 1 in your KVC-compliant class. For best performance implement a replacement method too.
*/
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

该方法的查找路径为:

  • 查找类中是否有-insertObject:inAtIndex:和-removeObjectFromAtIndex:(同NSMutableArray的-insert:atIndexes:和-removeObjectFromAtIndex:方法),如果找到至少一个insert和至少一个remove方法。
  • 查找类中是否有-set:方法,如果有则调用这个方法进行赋值
  • accessInstanceVariablesDirectly方法是否返回YES,如果返回YES查找类中是否有对应名称为_ 或者 的成员变量(必须是NSMutableArray或者子类) 给其赋值
  • 调用-setValue:forUndefinedKey:方法默认为NSUndefinedKeyException的crash

疑问?

在尝试写测试代码时,按照valueForKey:方法的实现想法,我们通过mutableArrayValueForKey获取一个可变数组然后进行增删改查。

1
2
3
4
5
6
7
- (void)mutableArrayValueForKey {
self.father = [Father new];
[[self.father mutableArrayValueForKey:@"children"] addObject:@"farmer"];
NSLog(@"mutableArrayValueForKey -- addObject %@",[self.father mutableArrayValueForKey:@"children"].firstObject);
[[self.father mutableArrayValueForKey:@"children"] removeObjectAtIndex:0];
NSLog(@"mutableArrayValueForKey -- removeObject %@",[self.father mutableArrayValueForKey:@"children"]);
}

当我直接运行上述代码时,Demo直接crash

1
2020-12-26 23:36:46.702341+0800 Runtime-KVC[51145:19283102] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Father 0x6000004dd260> valueForUndefinedKey:]: this class is not key value coding-compliant for the key children.'

valueForUndefinedKey 这里提示我们father类中没有children这个属性,但是在使用valueForKey时,我们即使key值是一个有问题的但并不影响我们的操作,我们依然可以实现数据的存取。但是这里就不可以! 如果有了解的同学可以给我留言,万分感谢。

mutableArrayValueForKey&KVO

在网上搜索这个方法的使用时,大部分都提到与KVO一起使用,下面我们来看下这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@interface Father : Person
@property (nonatomic, strong) NSMutableArray *children;
@end

@implementation Father

- (instancetype)init {
if (self = [super init]) {
_children = [NSMutableArray array];
[self addObserver];
}
return self;
}

- (void)addObserver {
[self addObserver:self forKeyPath:@"children" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)removeObserver {
[self removeObserver:self forKeyPath:@"children"];
}

- (void)dealloc {
[self removeObserver];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"children"]) {
NSLog(@"%@",change);
}
}
@end


// 外部使用
- (void)mutableArrayValueForKey {
self.father = [Father new];
[self.father.children addObject:@"son1"];
}

控制台输出为空,这里可以看出即使我们添加了KVO监听数组的变化我们仍无法在数组元素改变时监听到数组改变。

但是当我们把调用改为

1
2
3
4
- (void)mutableArrayValueForKey {
self.father = [Father new];
[[self.father mutableArrayValueForKey:@"children"] addObject:@"farmer"];
}

控制台输出变为:

1
2
3
4
5
6
7
8
2020-12-26 23:52:17.776620+0800 Runtime-KVC[51402:19295413] insertObject: farmer inChildrenAtIndex: 1
2020-12-26 23:52:17.776865+0800 Runtime-KVC[51402:19295413] {
indexes = "<_NSCachedIndexSet: 0x600003278320>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
farmer
);
}

显然通过mutableArrayValueForKey方法获取father对象的children数组时触发了KVO对数组的监听。在联想到mutableArrayValueForKey查找路径,因为默认我们没有实现第一步查找的方法会自动走到第二步通过setKey方法进行赋值,而KVO的实现原理就是通过重写Set方法进行监听,因此我们使用mutableArrayValueForKey就可以实现对数组的监听。

同样我们可以使用下面的方式验证:

1
2
3
4
5
6
7
- (void)mutableArrayValueForKey {
self.father = [Father new];
[self.father.children addObject:@"son1"];
NSLog(@"befor ---- children = %p",self.father.children);
[[self.father mutableArrayValueForKey:@"children"] addObject:@"farmer"];
NSLog(@"after ---- children = %p",self.father.children);
}

控制台输出结果:

1
2
3
4
5
6
7
8
9
2020-12-27 00:07:09.558349+0800 Runtime-KVC[51645:19310184] befor ---- children = 0x600000a81020
2020-12-27 00:07:09.558632+0800 Runtime-KVC[51645:19310184] {
indexes = "<_NSCachedIndexSet: 0x6000004d0ca0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
farmer
);
}
2020-12-27 00:07:09.558717+0800 Runtime-KVC[51645:19310184] after ---- children = 0x600000a80f90

从打印结果我们可以看出在调用了mutableArrayValueForKey方法后,self.father.children的对象地址就发生了改变。

加入我们再想数组中继续添加数据

1
2
3
4
5
6
7
8
9
- (void)mutableArrayValueForKey {
self.father = [Father new];
[self.father.children addObject:@"son1"];
NSLog(@"befor ---- children = %p children class %@",self.father.children,[self.father.children class]);
[[self.father mutableArrayValueForKey:@"children"] addObject:@"farmer"];
NSLog(@"after ---- children = %p children class %@",self.father.children,[self.father.children class]);
[[self.father mutableArrayValueForKey:@"children"] addObject:@"air"];
NSLog(@"after ---- children = %p children class %@",self.father.children,[self.father.children class]);
}

控制台打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2020-12-27 00:11:15.866438+0800 Runtime-KVC[51719:19314394] befor ---- children = 0x600003330360  children class __NSArrayM
2020-12-27 00:11:15.866768+0800 Runtime-KVC[51719:19314394] {
indexes = "<_NSCachedIndexSet: 0x600003d57c40>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
farmer
);
}
2020-12-27 00:11:15.866870+0800 Runtime-KVC[51719:19314394] after ---- children = 0x600003330900 children class __NSArrayM
2020-12-27 00:11:15.867033+0800 Runtime-KVC[51719:19314394] {
indexes = "<_NSCachedIndexSet: 0x600003d57c60>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
kind = 2;
new = (
air
);
}
2020-12-27 00:11:15.867115+0800 Runtime-KVC[51719:19314394] after ---- children = 0x6000033590b0 children class __NSArrayM

三次添加元素数组的地址分别为: 0x600003330360,0x600003330900,0x6000033590b0 由此可见每次我们修改数组时都是重新创建一个数组然后调用setter方法重新赋值而非修改之前的数组。

如果我们每次增删改的时候都要重新创建数组,一旦我们的数组元素比较多时,效率肯定会受到一定的影响。所以这类方法一定要慎用。

但是如果我们必须要使用时,通过上面我们对查找顺序的了解我们需要在Father类中实现

1
2
3
4
5
6
7
8
9
- (void)removeObjectFromChildrenAtIndex:(NSUInteger)index {
NSLog(@"removeObjectFromChildrenAtIndex %@",@(index));
[self.children removeObjectAtIndex:index];
}

- (void)insertObject:(NSString *)object inChildrenAtIndex:(NSUInteger)index {
NSLog(@"insertObject: %@inChildrenAtIndex: %@",object,@(index));
[self.children insertObject:object atIndex:index];
}

实现这两个方法后,就可以保证在第一步查找时就可以找到对应的方法,这样就不需要走setter方法,我们也可以实现KVO的监听。

我们可以通过下面代码验证:

1
2
3
4
5
6
7
8
9
- (void)mutableArrayValueForKey {
self.father = [Father new];
[self.father.children addObject:@"son1"];
NSLog(@"befor ---- children = %p children class %@",self.father.children,[self.father.children class]);
[[self.father mutableArrayValueForKey:@"children"] addObject:@"farmer"];
NSLog(@"after ---- children = %p children class %@",self.father.children,[self.father.children class]);
[[self.father mutableArrayValueForKey:@"children"] addObject:@"air"];
NSLog(@"after ---- children = %p children class %@",self.father.children,[self.father.children class]);
}

控制台输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2020-12-27 00:30:06.297870+0800 Runtime-KVC[51979:19331763] befor ---- children = 0x6000008b9a40  children class __NSArrayM
2020-12-27 00:30:06.298069+0800 Runtime-KVC[51979:19331763] insertObject: farmerinChildrenAtIndex: 1
2020-12-27 00:30:06.298360+0800 Runtime-KVC[51979:19331763] {
indexes = "<_NSCachedIndexSet: 0x6000006d02e0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
farmer
);
}
2020-12-27 00:30:06.298480+0800 Runtime-KVC[51979:19331763] after ---- children = 0x6000008b9a40 children class __NSArrayM
2020-12-27 00:30:06.298595+0800 Runtime-KVC[51979:19331763] insertObject: airinChildrenAtIndex: 2
2020-12-27 00:30:06.298754+0800 Runtime-KVC[51979:19331763] {
indexes = "<_NSCachedIndexSet: 0x6000006d0300>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
kind = 2;
new = (
air
);
}
2020-12-27 00:30:06.298854+0800 Runtime-KVC[51979:19331763] after ---- children = 0x6000008b9a40 children class __NSArrayM

从上面的log中我们可以看到在直接调用addObject和使用mutableArrayValueForKey方法获取到数组后在进行添加操作数组对象并没有发生改变均为0x6000008b9a40

但是我们同时也发现了一个问题,在没有调用setter方法时我们也可以通过KVO监听到了数据的变化?

因此,我们猜想必定是子类也重写了对应的方法,然后在方法的前后调用了willChange和didChange方法,因此我们先在Father类中实现willChange和didChange的集合方法

1
2
3
4
5
6
7
8
9
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key {
[super willChange:changeKind valuesAtIndexes:indexes forKey:key];
NSLog(@"willChange: valuesAtIndexes: forKey:%@",key);
}

- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key {
[super didChange:changeKind valuesAtIndexes:indexes forKey:key];
NSLog(@"didChange: valuesAtIndexes: forKey:%@",key);
}

接下来我们在进行测试:

1
2
3
4
- (void)mutableArrayValueForKey {
self.father = [Father new];
[[self.father mutableArrayValueForKey:@"children"] addObject:@"farmer"];
}

控制台输出:

1
2
3
4
5
6
7
8
9
10
2020-12-27 12:26:07.867838+0800 Runtime-KVC[56961:19600302] willChange: valuesAtIndexes: forKey:children
2020-12-27 12:26:07.868068+0800 Runtime-KVC[56961:19600302] insertObject: farmerinChildrenAtIndex: 1
2020-12-27 12:26:07.868480+0800 Runtime-KVC[56961:19600302] {
indexes = "<_NSCachedIndexSet: 0x600001a17b80>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
farmer
);
}
2020-12-27 12:26:07.868618+0800 Runtime-KVC[56961:19600302] didChange: valuesAtIndexes: forKey:children

从日志我们看出在修改方法`insertObject:inChildrenAtIndex:的前后willChange: valuesAtIndexes: forKey:children和didChange: valuesAtIndexes: forKey:children方法被调用我们通过断点看下

从上图我们可以看出willChange: valuesAtIndexes: forKey:children方法是在NSKeyValueNotifyingMutableArray._mutableArray 调用addObject时被调用。
很明显NSKeyValueNotifyingMutableArray就是KVO动态创建的子类,即[self.father mutableArrayValueForKey:@”children”]返回的集合代理类,这个类也实现了removeObjectFromChildrenAtIndex:和insertObject:inChildrenAtIndex:两个方法,并在方法调用前后分别调用了KVO的方法通知给外部。

KVC 应用

除了我们常用的获取对象的某些属性值以外,还有一些高级用法比如:

常规操作符

  • @sum:值的总和
  • @avg:平均值
  • @count:总个数
  • @max:最大值
  • @min:最小值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)advanceKVCMethod {
Person *p1 = [Person new];
p1.age = 10;

Person *p2 = [Person new];
p2.age = 20;

Person *p3 = [Person new];
p3.age = 30;

Person *p4 = [Person new];
p4.age = 40;

NSArray *pArray = @[p1,p2,p3,p4];

NSNumber *ageSum = [pArray valueForKeyPath:@"@sum.age"];
NSLog(@"年龄的总和为 %@",ageSum);

NSNumber *average = [pArray valueForKeyPath:@"@avg.age"];
NSLog(@"年龄的平均值为 %@",average);

NSNumber *count = [pArray valueForKeyPath:@"@count"];
NSLog(@"年龄的个数 %@",count);

NSNumber *max = [pArray valueForKeyPath:@"@max.age"];
NSLog(@"年龄最大的为 %@",max);

NSNumber *min = [pArray valueForKeyPath:@"@min.age"];
NSLog(@"年龄最小的为 %@",min);
}

打印结果:

1
2
3
4
5
2020-12-27 12:58:50.510127+0800 Runtime-KVC[57474:19627879] 年龄的总和为 100
2020-12-27 12:58:50.510338+0800 Runtime-KVC[57474:19627879] 年龄的平均值为 25
2020-12-27 12:58:50.510549+0800 Runtime-KVC[57474:19627879] 年龄的个数 4
2020-12-27 12:58:50.510718+0800 Runtime-KVC[57474:19627879] 年龄最大的为 40
2020-12-27 12:58:50.510872+0800 Runtime-KVC[57474:19627879] 年龄最小的为 10

集合去重

  • @distinctUnionOfObjects:元素唯一,会进行去重
  • @unionOfObjects:不会去重
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)advanceKVCDistinctMethod {
Person *p1 = [Person new];
p1.age = 10;

Person *p2 = [Person new];
p2.age = 20;

Person *p3 = [Person new];
p3.age = 20;

Person *p4 = [Person new];
p4.age = 30;

NSArray *pArray = @[p1,p2,p3,p4];

NSArray *distinctUnionOfObjectsArray=[pArray valueForKeyPath:@"@distinctUnionOfObjects.age"];
NSLog(@"distinctUnionOfObjects:%@",distinctUnionOfObjectsArray);

NSArray *unionOfObjectsArray=[pArray valueForKeyPath:@"@unionOfObjects.age"];
NSLog(@"unionOfObjects:%@",unionOfObjectsArray);
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
2020-12-27 13:16:42.349274+0800 Runtime-KVC[57721:19644904] distinctUnionOfObjects:(
10,
20,
30
)
2020-12-27 13:16:42.349493+0800 Runtime-KVC[57721:19644904] unionOfObjects:(
10,
20,
20,
30
)

总结

这篇文章我们主要从setValue:forKey:和valueForyKey:以及集合类型的mutableArrayValueForKey:去讲解KVC的实现和查找策略,让我们对KVC的实现有了一个比较清楚的认知,最后我们还介绍了几种KVC的高级用法,在某些场景也可以减少我们遍历的依赖。

参考

KVOMutableArray的分析和理解

详解Key-Value Coding源码

本文demo