一、KVC探索
KVC赋值步骤:
- 按照这个顺序查找名为set< Key >:或_set< Key >的第一个访问器。如果找到,使用输入值(或根据需要取消包装的值)调用它并完成。
- 如果没有找到简单的访问器,并且类方法
accessInstanceVariablesDirectly
返回YES,那么按顺序查找一个名称为_< key >、_is< key >、< key >或is< key >的实例变量。如果找到,直接用输入值(或取消包装的值)设置变量,然后完成。 - 当发现没有访问器或实例变量时,调用
setValue:forUndefinedKey:
。这将在默认情况下引发异常,档NSObject
的子类可能会提供特定于键的行为。
现在我们通过一个例子来理解:
@interface NYPerson : NSObject
{
@public
NSString *_name;
}
@end
@implementation NYPerson
- (void)setName:(NSString*)name {
NSLog(@"%s",__func__);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"123");
NYPerson *p = [NYPerson alloc];
[p setValue:@"NY" forKey:@"name"];
NSLog(@"%@",[p valueForKey:@"name"]);
}
复制代码
运行打印:
根据KVC 1.步骤,按照顺序查找setname找到直接赋值
。由于我们重写了set方法
,所以没有给_name赋值。所以打印(null).
接着按KVC 2.步骤修改代码:
@interface NYPerson : NSObject
{
@public
NSString *name;
NSString *_name;
NSString *_isName;
NSString *isName;
}
@implementation NYPerson
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
@end
NYPerson *p = [NYPerson alloc];
[p setValue:@"NY" forKey:@"name"];
NSLog(@"name = %@",p->name);
NSLog(@"_name = %@",p->_name);
NSLog(@"_isName = %@",p->_isName);
NSLog(@"isName = %@",p->isName);
复制代码
查看打印:
根据KVC 2.步骤理论,会优先给_< key >赋值。所以只打印了 _name = NY
根据KVC 3.步骤理论运行,查看打印: 发现控制打印报错setValue:forUndefinedKey:
。没有找到对应的key值。
KVC取值步骤:
-
(1-0)在实例中搜索找到的名称为get< key >,< key >,按此顺序是< key >或_< key >的第一个访问器方法。如果找到了,调用它并使用结果继续执行步骤5.否则执行下一步。
-
(2-0)如果没有找到简单的访问器方法,在实例中搜索其名称与模式
countOf< key> and objectIn< key>AtIndex:(对应于NSArray类定义的基本方法)
和< key>AtIndexes:(对应于NSArray方法objectsAtIndexes)匹配的方法。 -
(2-1)如果找到其中的第一个和另外两个中的至少一个,创建一个集合代理对象,响应所有
NSArray方法
并返回该对象。否则执行步骤3. -
(2-2)代理对象随后将接收到的任何NSArray消息转换为
countOf< key> and objectIn< key>AtIndex:
和 < key>AtIndexes:消息的组合,并将这些消息转换为创建它的符合键编码的对象。如果原始对象还实现了名为get< key>:range:的可选方法
,代理对象也会在适当的时候使用该方法。实际上,与键值编码兼容的对象一起工作的代理对象允许底层属性的行为就像NSArray一样,即使它不是。 -
(3-0)如果没有找到简单的访问器方法或数组访问方法组,则查找名为
countOf< key>、enumeratorOf< key>和memberOf< key>
的三组方法(对应与NSSet类定义的原语方法)。 -
(4-0)如果找到了所有三个方法,创建一个集合代理对象,该对象响应所有NSSet方法并返回该方法。否则执行步骤4.
-
(5-0)该代理对象随后将接收到的任何NSSet消息转换为
countOf< key>、enumeratorOf< key>和memberOf< key>:
消息的组合,这些消息将被发送给创建它的对象。实际上,与键值编码兼容的对象一起工作的代理对象允许底层属性的行为就像它是NSSet一样,即使它不是。 -
(6-0)如果没有找到简单的访问器方法或集合访问方法组,并且接收方的类方法
accessInstanceVariablesDirect
返回YES,按顺序搜索一个名为_< key>,_is< Key>,< key>,或is< key>的实例变量。如果找到,直接获取实例变量的值并继续步骤5.否则执行步骤6. -
(7-0)如果检索到的属性值是一个对象指针,只需返回结果。
-
(7-1)如果是NSNumber支持的标量类型,则存储在NSNumber实例中并返回该实例。
-
(7-2)如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回该对象。
-
(7-3)如果所有这些都失败了,调用valueForUndefinedKey:。这将在默认情况下引发异常,但NSObject的子类可能会提供特定于键的行为。
修改上面代码:
@interface NYPerson : NSObject
{
@public
NSString *name;
}
@end
@implementation NYPerson
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
- (void)getName {
NSLog(@"%s",__func__);
}
- (void)name {
NSLog(@"%s",__func__);
}
@end
NSLog(@"123");
NYPerson *p = [NYPerson alloc];
[p setValue:@"NY" forKey:@"name"];
NSLog(@"%@",[p valueForKey:@"name"]);
复制代码
根据取值 KVC 1.步骤 会按顺序找到 getName name等方法取值,但是get方法并没有返回值所以打印null。
继续修改代码: 由于,修改的setName并不是一个set方法。所以使用默认的set get的方法。打印NY。
根据取值 KVC 8.步骤 修改代码验证:
@interface NYPerson : NSObject
{
@public
NSString *name;
NSString *_name;
NSString *_isName;
NSString *isName;
}
@end
@implementation NYPerson
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
@end
NSLog(@"123");
NYPerson *p = [NYPerson alloc];
p->name = @"1";
p->_name = @"2";
p->_isName = @"3";
p->isName = @"4";
[p setValue:@"NY" forKey:@"name"];
NSLog(@"%@",[p valueForKey:@"name"]);
复制代码
最后被 setValue覆盖,所以打印NY。
修改代码在打印: 根据取值 KVC 8.步骤 按顺序 key _key _isKey isKey 依次取值。这里不一一演示打印了。
在根据 KVC 9.步骤 修改代码:
@interface NYPerson : NSObject
{
@public
}
@end
@implementation NYPerson
+(BOOL)accessInstanceVariablesDirectly
{
return NO;
}
- (id)valueForUndefinedKey:(NSString *)key
{
NSLog(@"%s",__func__);
return nil;
}
@end
复制代码
根据 KVC 9.步骤 没有相关 key _key _isKey isKey 的值,会执行valueForUndefinedKey
并报错,因为这里我重写了valueForUndefinedKey
所以打印null。
二、KVO的底层原理
我们直接上KVO的例子代码:
@interface NYPerson : NSObject
{
@public
NSString *_hobby;
}
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *age;
+(instancetype)shareNYPerson;
@end
@implementation NYPerson
+(instancetype)shareNYPerson {
static NYPerson *p = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
p = [NYPerson new];
});
return p;
}
@end
// 创建KVO 三步
self.p = [NYPerson new];
//1.创建监听
[self.p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:NULL];
self.p.name = @"ny";
//2.监听回调
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
//3.移除监听
-(void)dealloc {
[self.p removeObserver:self forKeyPath:@"name"];
}
复制代码
运行打印:
如果把第3步删除,并且修改[NYPerson new];
改成单例。
self.p = [NYPerson shareNYPerson];
复制代码
在运行代码:
多点几次,发现崩溃报错了,并且发现在奔溃时,执行了_NSSetObjectValueAndNotify ()
并且提示了_changeValueForKey:key:key:usingBlock:
函数。
为什么呢?如果NYPerson是单例,是内存存放在静态区。重复注册监听p.name属性。同时收到两条ValueAndNotify通知并更改就会触发报错。
只需要在NYPerson中加入:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
复制代码
就可以关闭KVO的触发。
在来看一个手动触发代码,大家应该都很熟悉:
-(void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
复制代码
我们接着看如果修改代码:
self.p = [NYPerson new];
[self.p addObserver:self forKeyPath:@"_hobby" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:NULL];
self.p.name = @"ny";
self.p->_hobby = @"sdsd";
NSLog(@"已运行KVO");
复制代码
运行结果:
结果是无法触发KVO,为什么呢?KVO内部的底层原理是什么样的呢?
我继续修改代码:[self.p setValue:@"sdsd" forKey:@"_hobby"];
在运行查看打印:
通过KVC的形势就可以触发KVO了,为什么会这样呢?
我们通过断点调试一步一步探索KVO:
在执行完addObserver后self.p的类发生了变化,变成了NSKVONotifying_NYPerson
通过打印,我们得知了在执行addObserver后self.p的类生成了一个子类NSKVONotifying_NYPerson
并且在NSKVONotifying_NYPerson
类中增加了setName:
方法。 并且会在[self.p removeObserver:self forKeyPath:@"name"];
之后移除这个NSKVONotifying_NYPerson
子类的指向重新指向NYPerson
。也就解释了为什么单例后不执行removeObserver的KVO会崩溃的原因。 通过打印得知,其实removeObserver
并没有删除NSKVONotifying_NYPerson
子类,只是把当前的p.isa 的指向重新指向了NYPerson
。 通过断点,看到在改变setName
之前系统还调用了willChangeValueForKey
和 didChangeValueForKey
小结
:
- 当p对象在执行
addObserver
的时候会动态生成一个NSKVONotifying_NYPerson
名称的子类。 - 并且在
NSKVONotifying_NYPerson
子类中添加setName:
方法。 - 把当前p对象的isa指向
NSKVONotifying_NYPerson
名称的子类。 - 在p对象
setName
时确实执行的是NSKVONotifying_NYPerson
名称的子类下的setName方法。 - 这个
setName
方法中其实增加了willChangeValueForKey
和didChangeValueForKey
方法,在GNUstep源码中可以看到KVO底层实现,就是在KVONotifying
通知中增加了这两个方法。 - 也可以通过
[self.p setValue:@"sdsd" forKey:@"_hobby"]
方式来触发KVO。