开启生长之旅!这是我参加「日新计划 12 月更文应战」的第7天,点击检查活动详情
一、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。