面试题iOS 开发笔记iOS底层

iOS KVO 详解

2019-10-04  本文已影响0人  萨缪

什么是KVO?

KVO 全称 Key Value Observing,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,只针对属性才会发生作用,一般继承自 NSObject 的对象都默认支持 KVO

KVO 可以监听单个属性的变化,也可以监听集合对象的变化。通过 KVCmutableArrayValueForKey: 等方法获得代理对象,当代理对象的内部对象发生改变时,会回调 KVO 监听的方法。集合对象包含 NSArrayNSSet

KVO基本使用

注册观察者

 /*
@observer:就是观察者,是谁想要观测对象的值的改变。
@keyPath:就是想要观察的对象属性。
@options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值。
@context:想要携带的其他信息,比如一个字符串或者字典什么的。
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

监听回调

/*
@keyPath:观察的属性
@object:观察的是哪个对象的属性
@change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
@context:上面添加观察者时携带的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

调用方式

自动调用

//通过属性的点语法间接调用
objc.name = @"";

// 直接调用set方法
[objc setName:@"Savings"];

// 使用KVC的setValue:forKey:方法
[objc setValue:@"Savings" forKey:@"name"];

// 使用KVC的setValue:forKeyPath:方法
[objc setValue:@"Savings" forKeyPath:@"account.name"];

手动调用

  1. 第二步我们需要重写setter方法
- (void)setName:(NSString *)name {
    if (name != _name) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
}

移除观察者

//需要在不使用的时候,移除监听
- (void)dealloc{
    [self removeObserver:self forKeyPath:@"age"];
}

Crash

观察者未实现监听方法

未及时移除观察者

Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)

//观察者ObserverPersonChage
@interface ObserverPersonChage : NSObject
  //实现observeValueForKeyPath: ofObject: change: context:
@end

//ViewController
- (void)addObserver
{
    self.observerPersonChange = [[ObserverPersonChage alloc] init];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"age" options:option context:@"age chage"];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"name" options:option context:@"name change"];
}

//点击按钮将观察者置为nil,即销毁
- (IBAction)clearObserverPersonChange:(id)sender {
    self.observerPersonChange = nil;
}

//点击改变person1属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 29;
    self.person1.name = @"hengcong";
}

  1. 假如在当前 ViewController 中,注册了观察者,点击屏幕,改变被观察对象 person1 的属性值。
  2. 点击对应按钮,销毁观察者,此时 self.observerPersonChange 为 nil。
  3. 再次点击屏幕,此时 Crash;

多次移除观察者

Cannot remove an observer for the key path “age” from because it is not registered as an observer.

实际应用

KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的iOS教程中有一个很经典的案例,通过KVOModelController之间进行通信。

image

KVO实现原理

KVO是通过isa 混写(isa-swizzling)技术实现的(是不是一脸懵逼?我第一次见和你一样,你现在只需要知道这个技术就行了,下面我会图文并茂的给你讲解到底是怎么回事。)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

测试代码

NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;

NSLog(@"person1添加KVO监听对象之前-类对象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO监听之前-方法实现 -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO监听之前-元类对象 -%@", object_getClass(object_getClass(self.person1)));

[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];

NSLog(@"person1添加KVO监听对象之后-类对象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO监听之后-方法实现 -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO监听之后-元类对象 -%@", object_getClass(object_getClass(self.person1)));

//打印结果
KVO-test[1214:513029] person1添加KVO监听对象之前-类对象 -Person
KVO-test[1214:513029] person1添加KVO监听之前-方法实现 -0x100411470
KVO-test[1214:513029] person1添加KVO监听之前-元类对象 -Person

KVO-test[1214:513029] person1添加KVO监听对象之后-类对象 -NSKVONotifying_Person
KVO-test[1214:513029] person1添加KVO监听之后-方法实现 -0x10076c844
KVO-test[1214:513029] person1添加KVO监听之后-元类对象 -NSKVONotifying_Person

//通过地址查找方法
(lldb) p (IMP)0x10f24b470
(IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x10f5a6844
(IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)

发现中间对象

从上述测试代码的结果我们发现,person 中的 isa 从开始指向Person类对象,变成指向了 NSKVONotifying_Person 类对象

image
  1. 使用KVO监听对象后,对象和类对象之间会添加一个中间对象
image
NSKVONotifying_Person类内部实现

我们从上面两张图很清楚的看到添加KVO之前和KVO之后的变化,下面我们剖析一下这个中间类NSKVONotifying_Person(这里是*通配符,它代表数据类型,例如:int, longlong)

- (void)setAge:(int)age{
    _NSSet*ValueAndNotify();//这个方法调用顺序是什么,它是在调用何处方法,都在setter方法改变中详解
}

- (Class)class {
    return [LDPerson class];
}

- (void)dealloc {
    // 收尾工作
}

- (BOOL)_isKVOA {
    return YES;
}

setter实现不同

KVO内部调用流程

<article class="_2rhmJa">

KVO扩展

1.KVC 与 KVO 的不同?

2.和 notification(通知)的区别?

</article>

上一篇下一篇

猜你喜欢

热点阅读