iOS 底层 day04 KVO

一、基础 KVO 的日常使用

一般情况,分如下三个步骤:

// 1. 进行监听
[self.person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];

// 2. 添加监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"是监听到了%@实例对象的%@值,改变了%@,context:%@", object, keyPath, change, context);
}
// 3. 销毁监听
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"height"];
}

二、两道经典面试题

1. iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么?)
  • 利用 RuntimeAPI 动态生成一个子类,并且让 instance 对象的 isa 指针指向这个全新子类
  • 这个全新子类会重写 KVO 属性的 set 方法,伪代码如下:
  • willChangeValueForKey:
  • 父类 setter 方法
  • didChangeValueForKey:
2. 如何手动触发 KVO?
  • willChangeValueForKey:
  • didChangeValueForKey:

三、探求 KVO 的本质,并从代码层面上求证

1. 绘图说明 KVO 的本质
KVO 原理图
  • KVO 对象生成了一个 继承自 SPPerson 类对象的 NSKVONotifying_SPPerson 类对象
  • 并将实例对象的 isa 指向 NSKVONotifying_SPPerson 类对象
  • 并且有四个方法,setHeight:classdealloc_isKVOA
  • 用图中的伪代码,重写了 setHeight: 方法
2. 代码观察被 KOV 实例对象的 isa 指针
  • 编写如下代码,并打上断点
- (void)viewDidLoad {
    [super viewDidLoad];
    
    SPPerson *person1 = [[SPPerson alloc] init];
    person1.height = 1;
    SPPerson *person2 = [[SPPerson alloc] init];
    person2.height = 2;
    NSLog(@"请在此处打断点1");
    [person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
    NSLog(@"请在此处打断点2");
}
  • 我们可以获得如下调试信息
(lldb) p person1->isa
(Class) $0 = SPPerson
(lldb) p person2->isa
(Class) $1 = SPPerson
2020-08-25 15:42:17.983376+0800 Demo-KVO[9298:415101] 请在此处打断点1
(lldb) p person1->isa
(Class) $2 = NSKVONotifying_SPPerson
(lldb) p person2->isa
(Class) $3 = SPPerson
  • 我们发现在 KOV 之后,person1 的 isa 指针,指向了一个 NSKVONotifying_SPPerson 的类对象
3. 代码观察NSKVONotifying_SPPerson类对象方法列表
- (void)viewDidLoad {
    [super viewDidLoad];
    
    SPPerson *person1 = [[SPPerson alloc] init];
    person1.height = 1;
    NSLog(@"KVO 之前 %@ 的方法列表:", object_getClass(person1));
    [self printClassMethods:object_getClass(person1)];
    [person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
    NSLog(@"KVO 之后  %@ 的方法列表:", object_getClass(person1));
    [self printClassMethods:object_getClass(person1)];
    
}
- (void)printClassMethods:(Class) cls {
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [[NSMutableString alloc] init];
    for (int i = 0 ; i < count; i++) {
        NSString *name = NSStringFromSelector(method_getName(methodList[i]));
        [methodNames appendString:name];
        [methodNames appendString:@"  ,  "];
    }
    NSLog(@"%@", methodNames);
}
  • 代码输出信息如下:
Demo-KVO[10337:501248] KVO 之前 SPPerson 的方法列表:
Demo-KVO[10337:501248] height  ,  setHeight:  ,  weight  ,  setWeight:  ,
Demo-KVO[10337:501248] KVO 之后  NSKVONotifying_SPPerson 的方法列表:
Demo-KVO[10337:501248] setHeight:  ,  class  ,  dealloc  ,  _isKVOA  ,

  • 由此可以看出 NSKVONotifying_SPPersonsetHeight: , class , dealloc , _isKVOA 这四个方法
  • 思考为什么没有 height 方法?因为它的父类有,所以它不需要多此一举
4. 代码观察被 KOV 实例对象的 set 方法
- (void)viewDidLoad {
    [super viewDidLoad];
    
    SPPerson *person1 = [[SPPerson alloc] init];
    person1.height = 1;
    NSLog(@"%p",[person1 methodForSelector:@selector(setHeight:)]);
    [person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
    NSLog(@"%p",[person1 methodForSelector:@selector(setHeight:)]);
}
  • 得到如下信息
Demo-KVO[9473:425751] 0x1010eaf60
Demo-KVO[9473:425751] 0x7fff258f10eb
(lldb) p (IMP)0x1010eaf60
(IMP) $1 = 0x00000001010eaf60 (Demo-KVO`-[SPPerson setHeight:] at SPPerson.h:15)
(lldb) p (IMP)0x7fff258f10eb
(IMP) $2 = 0x00007fff258f10eb (Foundation`_NSSetIntValueAndNotify)
  • 说明 KVO 之后 setHeight:方法被替换了成了 _NSSetIntValueAndNotify 方法
5. _NSSetIntValueAndNotify 是一个 C 语言方法,因为 Foundation 框架并不开源,所以我们只能从侧面猜测它的内部实现。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person1 willChangeValueForKey:@"height"];
    [self.person1 didChangeValueForKey:@"height"];
}
  • 打印如下
2020-08-25 16:48:15.816554+0800 Demo-KVO[10176:485066] 是监听到了<SPPerson: 0x600003eb0230>实例对象的height值,改变了{
    kind = 1;
    new = 1;
    old = 1;
},context:123
  • 我们实现上述代码,我们发现可以不直接改变 person1 的height 的数值,也可以触发- (void)observeValueForKeyPath:ofObject:change:context:
  • 所以我们可以猜测,KVO 实现 setHeight: 方法的伪代码如下:
- (void)setHeight:(int)height {
    [self willChangeValueForKey:@"height"];
    [super setHeight:value];
    [self didChangeValueForKey:@"height"];
}

KVO补充

1、如果在B页面使用苹果原生的KVO写法监听了一个全局对象的值,此时退出B页面(B页面销毁,且未移除KVO监听),回到A页面触发全局对象值修改,程序会怎么样?

  • 会崩溃
  • 报错:_NSSetIntValueAndNotify
image.png

2、如何解决上述问题?就是监听者被销毁了,但是KVO还没移除监听?

  • 使用YYKit的扩展KVO监听
  • 关键原理:用关联技术,创建一个关联属性,将关联属性当做监听者;从而实现实例销毁,监听者也自动销毁,然后KVO监听也自动销毁。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。