开启生长之旅!这是我参加「日新计划 12 月更文应战」的第7天,点击检查活动详情

一、KVC探索

KVC赋值过程:

  1. 按照这个次序查找名为set< Key >:或_set< Key >的第一个拜访器。假如找到,运用输入值(或依据需要撤销包装的值)调用它并完结。
  2. 假如没有找到简略的拜访器,而且类办法accessInstanceVariablesDirectly回来YES,那么按次序查找一个称号为_< key >、_is< key >、< key >或is< key >的实例变量。假如找到,直接用输入值(或撤销包装的值)设置变量,然后完结。
  3. 当发现没有拜访器或实例变量时,调用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"]);
}

运转打印:

iOS之KVC和KVO底层探索
依据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);

检查打印:

iOS之KVC和KVO底层探索
依据KVC 2.过程理论,会优先给_< key >赋值。所以只打印了 _name = NY

iOS之KVC和KVO底层探索
依据KVC 3.过程理论运转,检查打印:
iOS之KVC和KVO底层探索
发现控制打印报错setValue:forUndefinedKey:。没有找到对应的key值。

KVC取值过程:

  1. (1-0)在实例中搜索找到的称号为get< key >,< key >,按此次序是< key >或_< key >的第一个拜访器办法。假如找到了,调用它并运用成果持续履行过程5.不然履行下一步。

  2. (2-0)假如没有找到简略的拜访器办法,在实例中搜索其称号与模式countOf< key> and objectIn< key>AtIndex:(对应于NSArray类界说的根本办法)和< key>AtIndexes:(对应于NSArray办法objectsAtIndexes)匹配的办法。

  3. (2-1)假如找到其中的第一个和别的两个中的至少一个,创立一个调集署理目标,响应一切NSArray办法并回来该目标。不然履行过程3.

  4. (2-2)署理目标随后将接收到的任何NSArray音讯转换为countOf< key> and objectIn< key>AtIndex:和 < key>AtIndexes:音讯的组合,并将这些音讯转换为创立它的契合键编码的目标。假如原始目标还实现了名为get< key>:range:的可选办法,署理目标也会在恰当的时分运用该办法。实际上,与键值编码兼容的目标一同作业的署理目标答应底层特点的行为就像NSArray相同,即便它不是。

  5. (3-0)假如没有找到简略的拜访器办法或数组拜访办法组,则查找名为countOf< key>、enumeratorOf< key>和memberOf< key>的三组办法(对应与NSSet类界说的原语办法)。

  6. (4-0)假如找到了一切三个办法,创立一个调集署理目标,该目标响应一切NSSet办法并回来该办法。不然履行过程4.

  7. (5-0)该署理目标随后将接收到的任何NSSet音讯转换为countOf< key>、enumeratorOf< key>和memberOf< key>:音讯的组合,这些音讯将被发送给创立它的目标。实际上,与键值编码兼容的目标一同作业的署理目标答应底层特点的行为就像它是NSSet相同,即便它不是。

  8. (6-0)假如没有找到简略的拜访器办法或调集拜访办法组,而且接收方的类办法accessInstanceVariablesDirect回来YES,按次序搜索一个名为_< key>,_is< Key>,< key>,或is< key>的实例变量。假如找到,直接获取实例变量的值并持续过程5.不然履行过程6.

  9. (7-0)假如检索到的特点值是一个目标指针,只需回来成果。

  10. (7-1)假如是NSNumber支撑的标量类型,则存储在NSNumber实例中并回来该实例。

  11. (7-2)假如成果是NSNumber不支撑的标量类型,则转换为NSValue目标并回来该目标。

  12. (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。

iOS之KVC和KVO底层探索
持续修正代码:
iOS之KVC和KVO底层探索
由于,修正的setName并不是一个set办法。所以运用默许的set get的办法。打印NY。
iOS之KVC和KVO底层探索

依据取值 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。

iOS之KVC和KVO底层探索
修正代码在打印:
iOS之KVC和KVO底层探索
依据取值 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

iOS之KVC和KVO底层探索

依据 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"];
}

运转打印:

iOS之KVC和KVO底层探索
假如把第3步删去,而且修正[NYPerson new];改成单例。

self.p = [NYPerson shareNYPerson];

在运转代码:

iOS之KVC和KVO底层探索
多点几回,发现溃散报错了,而且发现在奔溃时,履行了_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");

运转成果:

iOS之KVC和KVO底层探索
成果是无法触发KVO,为什么呢?KVO内部的底层原理是什么样的呢?

我持续修正代码:[self.p setValue:@"sdsd" forKey:@"_hobby"];在运转检查打印:

iOS之KVC和KVO底层探索
经过KVC的局势就能够触发KVO了,为什么会这样呢?

咱们经过断点调试一步一步探索KVO:

iOS之KVC和KVO底层探索
在履行完addObserver后self.p的类发生了改变,变成了NSKVONotifying_NYPerson
iOS之KVC和KVO底层探索
经过打印,咱们得知了在履行addObserver后self.p的类生成了一个子类NSKVONotifying_NYPerson
iOS之KVC和KVO底层探索
而且在NSKVONotifying_NYPerson类中增加了setName:办法。
iOS之KVC和KVO底层探索
而且会在[self.p removeObserver:self forKeyPath:@"name"];之后移除这个NSKVONotifying_NYPerson子类的指向从头指向NYPerson。也就解释了为什么单例后不履行removeObserver的KVO会溃散的原因。
iOS之KVC和KVO底层探索
经过打印得知,其实removeObserver并没有删去NSKVONotifying_NYPerson子类,只是把当时的p.isa 的指向从头指向了NYPerson
iOS之KVC和KVO底层探索
iOS之KVC和KVO底层探索
经过断点,看到在改变setName之前体系还调用了willChangeValueForKeydidChangeValueForKey

小结

  1. 当p目标在履行addObserver的时分会动态生成一个NSKVONotifying_NYPerson称号的子类。
  2. 而且在NSKVONotifying_NYPerson子类中添加setName:办法。
  3. 把当时p目标的isa指向NSKVONotifying_NYPerson称号的子类。
  4. 在p目标setName时确实履行的是NSKVONotifying_NYPerson称号的子类下的setName办法。
  5. 这个setName办法中其实增加了willChangeValueForKeydidChangeValueForKey办法,在GNUstep源码中能够看到KVO底层实现,就是在KVONotifying通知中增加了这两个办法。
  6. 也能够经过[self.p setValue:@"sdsd" forKey:@"_hobby"]方法来触发KVO。