先来一个例子
// -------------------- Cat --------------------
@interface Cat : NSObject
@property (nonatomic, assign) NSInteger age;
@end
@implementation Cat
@end
// -------------------- ViewController --------------------
@interface ViewController ()
@property (nonatomic, strong) Cat *cat;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.cat = [[Cat alloc] init];
// 赋值操作
[self.cat setValue:@2 forKey:@"age"];
// 取值操作 + 输出
NSLog(@"age = %@", [self.cat valueForKey:@"age"]);
}
@end
输出:
2021-01-02 16:51:03.951534+0800 KVC[89501:2003082] age = 2
嗯,测试有效,毕竟这只是一个简单的 KVC 操作。
在实际开发中 KVC 的赋值操作有下面方法:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
取值操作有:
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
其中 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; 方法的功能会比
- (void)setValue:(nullable id)value forKey:(NSString *)key; 方法强大一些,它能为对象里面属性的属性进行赋值。(可以连接多个属性)
比如如果 cat 对象里面有一个 fish 属性对象,fish 对象里面有一个 count 属性那么可以这样操作:
[self.cat setValue:@100 forKeyPath:@"fish.count"];
那么一个 KVC 的赋值操作是怎样进行的呢?其实在苹果的使用接口里面有很详细的说明:
setValue: forKey: 方法:
/* 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;
valueForKey: 方法:
/* 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;
赋值和取值的操作可归纳为如下流程图:
赋值操作
setValue: forKey: 方法:
其中 accessInstanceVariablesDirectly 方法的默认返回值是 YES
取值操作
valueForKey: 方法:
验证一下
验证赋值操作
下面是一个验证赋值操作的测试代码,在实际的测试中可尝试注释掉不同的方法或者成员变量查看具体的输出情况。通过测试可以验证的确和上面归纳的赋值流程图的输出是一致的。
@interface Cat : NSObject {
// 测试的时候可注释不同的变量,然后查看具体的情况
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
// 把 age 属性注释掉,自己创建 set 和 get 方法
//@property (nonatomic, assign) NSInteger age;
@end
@implementation Cat
// 赋值操作,优先级1
- (void)setAge:(NSInteger)age {
_age = age;
}
// 赋值操作,优先级2
- (void)_setAge:(NSInteger)age {
_age = age;
}
// 默认 true
+ (BOOL)accessInstanceVariablesDirectly {
// 测试的时候可切换成 false,可查看是否会奔溃然后抛出异常
return true;
}
@end
验证取值操作
同理可验证取值操作也是如流程图一样。
@interface Cat : NSObject {
// 测试的时候可注释不同的变量,然后查看具体的情况
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
// 把 age 属性注释掉,自己创建 set 和 get 方法
//@property (nonatomic, assign) NSInteger age;
@end
@implementation Cat
// 取值操作,优先级1
- (NSInteger)getAge {
return _age;
}
// 取值操作,优先级2
- (NSInteger)age {
return _age;
}
// 取值操作,优先级3
- (NSInteger)isAge {
return _age;
}
// 取值操作,优先级4
- (NSInteger)_age {
return _age;
}
// 默认 true
+ (BOOL)accessInstanceVariablesDirectly {
// 测试的时候可切换成 false,可查看是否会奔溃然后抛出异常
return true;
}
@end
和 KVO 的关系
通过上面的代码验证可能会想:那么对一个属性设置 KVC 以后,如果它有相应的 KVO 监听,那么设置 KVC 会触发 KVO 吗?
验证对属性的 KVC 操作是否会触发 KVO
这是 Cat 类的测试代码:
@interface Cat : NSObject
// 直接添加属性 age 查看 KVC 情况
@property (nonatomic, assign) NSInteger age;
@end
@implementation Cat
// 下面两个方法主要是查看一下调用顺序
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"-----willChangeValueForKey: begin");
[super willChangeValueForKey:key];
NSLog(@"-----willChangeValueForKey: end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"-----didChangeValueForKey: begin");
[super didChangeValueForKey:key];
NSLog(@"-----didChangeValueForKey: end");
}
@end
实际的触发代码:
@interface ViewController ()
@property (nonatomic, strong) Cat *cat;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.cat = [[Cat alloc] init];
self.cat.age = 2; // 初始化为 2
// 添加 KVO 监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.cat addObserver:self forKeyPath:@"age" options:options context:@"Kitten"];
}
// KVO 的监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == @"Kitten") {
NSLog(@"KVO监听 %@, %@, %@", object, keyPath, change);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// 点击测试
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 使用 KVC 的方式验证是否会触发 KVO
[self.cat setValue:@5 forKey:@"age"];
}
// 合适的时候移除 KVO
- (void)dealloc {
[self.cat removeObserver:self forKeyPath:@"age"];
}
@end
编译运行,点击屏幕测试,输出如下:
2021-01-02 17:20:42.972312+0800 KVO[90787:2033943] -----willChangeValueForKey: begin
2021-01-02 17:20:42.972504+0800 KVO[90787:2033943] -----willChangeValueForKey: end
2021-01-02 17:20:42.972609+0800 KVO[90787:2033943] -----didChangeValueForKey: begin
2021-01-02 17:20:42.972854+0800 KVO[90787:2033943] KVO监听 <Cat: 0x600003af4240>, age, {
kind = 1;
new = 5;
old = 2;
}
2021-01-02 17:20:42.972989+0800 KVO[90787:2033943] -----didChangeValueForKey: end
可以看出 如果直接对属性进行 KVC 操作,如果属性添加了 KVO,那么是会触发 KVO 的。毕竟属性 KVC 的时候会先去对它的 setKey 方法进行设置,而通过 看看 Objective-C 里面的 KVO 这篇文章的分析可知对 setKey 方法进行操作会触发 KVO。
验证有成员变量没属性的情况 KVC 操作是否会触发 KVO
如果对象没有属性但是有成员变量,那么默认情况也就没有 setKey 方法了,所以根据上面的流程图也就知道如果此时对成员变量进行 KVC 操作,走的是直接赋值操作。
修改 Cat 类的测试代码
@interface Cat : NSObject {
// 不要 age 属性了,改为直接添加成员变量测试 KVC 情况
@public
NSInteger _age;
}
@end
@implementation Cat
// 下面两个方法主要是查看一下调用顺序
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"-----willChangeValueForKey: begin");
[super willChangeValueForKey:key];
NSLog(@"-----willChangeValueForKey: end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"-----didChangeValueForKey: begin");
[super didChangeValueForKey:key];
NSLog(@"-----didChangeValueForKey: end");
}
@end
ViewController 类里面其它保持不变,修改初始化赋值操作:
self.cat = [[Cat alloc] init];
self.cat->_age = 3; // 修改成 3, 看一下差异
编译运行,点击屏幕测试,输出如下:
2021-01-02 17:38:01.537423+0800 KVO[91417:2045259] -----willChangeValueForKey: begin
2021-01-02 17:38:01.537619+0800 KVO[91417:2045259] -----willChangeValueForKey: end
2021-01-02 17:38:01.537734+0800 KVO[91417:2045259] -----didChangeValueForKey: begin
2021-01-02 17:38:01.537977+0800 KVO[91417:2045259] KVO监听 <Cat: 0x600003b14150>, age, {
kind = 1;
new = 5;
old = 3;
}
2021-01-02 17:38:01.538102+0800 KVO[91417:2045259] -----didChangeValueForKey: end
从输出可看出: 如果对象没有对应属性,而是直接对它的成员变量进行 KVC 操作也会触发 KVO。
为什么会这样呢?我们知道 KVO 的触发可以通过 setKey 自动触发,也可以自己调用:
[self willChangeValueForKey:@"xxx"];
[self didChangeValueForKey:@"xxx"];
这两个方法进行手动触发。所以可以猜想苹果如果对属性进行了 KVC 操作,那么它应该在某个时刻调用了
[self willChangeValueForKey:@"xxx"];
[self didChangeValueForKey:@"xxx"];
这两个方法。所以也就能触发 KVO 了,毕竟看代码的输出也的确是打印了相关信息的。