iOS - KVO
[toc]
参考
【 IOS-详解KVO底层实现 】
【 [深入浅出Cocoa]详解键值观察(KVO)及其实现机理 】
【 自动移除observer的KVO和Notification 】
观察者模式:
观察者模式定义了一种一对多的依赖关系, 让多个观察者对象同时监听某一个目标对象。这个目标对象在状态上发生变化时, 会通知所有观察者对象。观察者模式较完美地将目标对象与观察者对象解耦。
iOS中观察者模式的实现有两种:Notification、KVO。
KVO简介
KVO ( Key-Value Observing) 是OC对"观察者"设计模式的一种实现。
和 KVC 类似, 在 OC 中要实现 KVO 则必须实现 <NSKeyValueObserving>
协议, 但由于 NSObject 已经实现了该协议, 因此几乎所有的OC对象都可以使用KVO。
The NSKeyValueObserving (KVO) informal protocol (非正式协议) defines a mechanism that allows objects to be notified of changes to the specified properties of other objects.
KVO 作用
KVO提供一种机制, 指定一个被观察对象, 当对象某个属性发生更改时, 观察者会获得通知, 并作出相应处理, 在MVC设计架构下的项目, KVO机制很适合实现model模型和view视图之间的通讯。
这种模式有利于两个类间的解耦合, 尤其是对于业务逻辑与视图控制这两个功能的解耦合。
利用它可以很容易实现视图组件和数据模型的分离, 当数据模型的属性值改变之后作为监听器的视图组件就会被激发, 激发时就会回调监听器自身。
KVO API
NSKeyValueObserving.h
KVO 关键方法定义在 NSKeyValueObserving.h
给NSObject添加的分类
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
NSKeyValueUnionSetMutation = 1,
NSKeyValueMinusSetMutation = 2,
NSKeyValueIntersectSetMutation = 3,
NSKeyValueSetSetMutation = 4
};
@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
@interface NSObject(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSObject(NSKeyValueObserverNotification)
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
@end
@interface NSObject(NSKeyValueObservingCustomization)
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
@end
给NSArray添加的分类
@interface NSArray<ObjectType>(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath;
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
给NSOrderedSet、NSSet添加的分类
@interface NSOrderedSet<ObjectType>(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSSet<ObjectType>(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
KVO使用方法
注册观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
-
observer:
观察者, 也就是KVO通知的订阅者。
订阅者必须实现observeValueForKeyPath:ofObject:change:context:
方法 -
keyPath:
描述将要观察的属性, 相对于被观察者。
-
options:
KVO的一些属性配置, 枚举值, 有四个选项。
NSKeyValueObservingOptionNew: // change字典包括改变后的值 NSKeyValueObservingOptionOld: // change字典包括改变前的值 NSKeyValueObservingOptionInitial: // 注册后立刻触发KVO通知 NSKeyValueObservingOptionPrior: // 值改变前是否也要通知 (这个key决定了是否在改变前改变后通知两次)
-
context:
上下文, 这个会传递到观察者的回调中, 可以为 kvo 的回调方法传值 (例如设定为一个放置数据的字典)
避免多次移除观察者导致crash: 由于 context 通常用来区分不同的KVO, 所以 context 的唯一性很重要。
通常, 我的使用方式是通过在当前.m文件里用静态变量定义。
static void *privateContext = 0;
观察者实现回调方法
在回调方法中处理属性发生变化时要做的事
// 当监听对象的属性值发生改变时, 该方法会被回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
-
keyPath:
被监听的keyPath, 用来区分不同的KVO监听
-
object:
被观察修改后的对象(可以通过object获得修改后的值)
-
change:
保存信息改变的字典(可能有旧的值,新的值等)
id oldValue = [change objectForKey:NSKeyValueChangeOldKey]; id newValue = [change objectForKey:NSKeyValueChangeNewKey];
-
context:
上下文, 用来区分不同的KVO监听, 注册观察者时context传过来的值
移除观察者
在观察者的dealloc中移除观察者 移除指定Key路径的监听器
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
KVO案例
@interface ViewController ()
@property (nonatomic, strong) Person *psn;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.psn = [[Person alloc] init];
// 运行到这里_psn isa = (Class) Person
[self.psn addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 运行到这里_psn isa = (Class) NSKVONotifying_Person
[_psn addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[_psn addObserver:self forKeyPath:@"profession" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
/// 如果有多个监听, 必须对触发回调函数的来源进行判断
if (object == _psn && [keyPath isEqualToString:@"name"]) {
NSString *nameOld = [change objectForKey:NSKeyValueChangeOldKey];
NSString *nameNew = change[NSKeyValueChangeNewKey];
NSLog(@"旧%@新%@", nameOld, nameNew);
}else if (object == _psn && [keyPath isEqualToString:@"hobby"]) {
NSString *hobbyOld = [change objectForKey:NSKeyValueChangeOldKey];
NSString *hobbyNew = change[NSKeyValueChangeNewKey];
NSLog(@"旧%@新%@", hobbyOld, hobbyNew);
}else if (object == _psn && [keyPath isEqualToString:@"profession"]) {
NSString *professionOld = [change objectForKey:NSKeyValueChangeOldKey];
NSString *professionNew = change[NSKeyValueChangeNewKey];
NSLog(@"旧%@新%@", professionOld, professionNew);
}else {
/// 如果被观察者的类还有父类, 并且其父类对象也绑定了其他KVO, 则当前类可能无法捕捉到这个KVO, 此时应该传给super处理。
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc {
[_psn removeObserver:self forKeyPath:@"name" context:nil];
[_psn removeObserver:self forKeyPath:@"hobby"];
[_psn removeObserver:self forKeyPath:@"profession"];
}
/**
潜在的问题有可能出现在dealloc中对KVO的注销上。
KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个keypath进行两次removeObserver时会导致程序crash,
这种情况常常出现在: 父类有一个KVO,父类在dealloc中remove了一次,子类又remove了一次。
不要以为这种情况很少出现!当你封装framework开源给别人用或者多人协作开发时是有可能出现的,而且这种crash很难发现。
一般代码中context字段都是nil,可否利用该字段来标识出到底KVO是superClass注册的,还是self注册的
我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash。
*/
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_psn.name = @"TimCook"; // 点语法调用setter可以激发KVO
_psn.hobby = @"football"; // 调用setter可以激发KVO
_psn->_hobby = @"tennis"; // 未调用setter, 不能激发KVO
[_psn setValue:@"engineer" forKey:@"profession"]; // 使用KVC, 可以激发KVO
/// KVO的关键就是监听对象的属性进行赋值时需调用setter方法或者KVC
}
@end
KVO实现原理
所用技术:
Apple使用了 isa混写(isa-swizzling) 来实现 KVO。
基本原理★★:
- 当类A的实例对象a第一次被添加观察(
addObserver
)时, KVO会利用 Runtime 动态创建一个继承自A的子类NSKVONotifying_A
, 并让a的isa指向这个全新的子类。 - 为新子类
NSKVONotifying_A
重写被观察的属性的setter
方法 (实际是替换了setter
的IMP
, 即_NSSetXxxValueAndNotify()
) 。 - 在
重写的setter
方法中, 给属性赋值前后, 分别调用-willChangeValueForKey:
和-didChangeValueForKey:
。 - 在
didChangeValueForKey:
中, 触发 observer 的监听方法observeValueForKeyPath:ofObject:change:context:
, 通知observer, 属性值发生了改变。 - 当某个 property 被移除观察者时, 删除其对应
重写的 setter 方法。 - 当没有 observer 观察任何一个 property 时, 删除动态创建的子类
NSKVONotifying_A
。
当修改 instance 对象的属性时, 调用setter方法, 实际是调用了 Foundation 的
_NSSetXxxValueAndNotify
函数// 看不到实现, 写的伪代码 void _NSSetXxxValueAndNotify() { [self willChangeValueForKey:@"xxx"]; // 调用父类的setter [super setXxx: xxx]; [self didChangeValueForKey:@"xxx"]; }
didChangeValueForKey:
内部会触发 Observer 的监听方法observeValueForKeyPath:ofObject:change:context:
// 看不到实现, 写的伪代码 - (void)didChangeValueForKey:(NSString *)key { // 通知 observer, xxx 属性值发生了改变 [observer observeValueForKeyPath:key ofObject:self change:nil context:nil]; }
原理剖析:
① 打断点可以看到:
在 addObserver
之前, 对象a的 isa指针 指向原始类 A
;
在 addObserver
走完, 对象a的 isa指针 指向中间类 NSKVONotifying_A
;
如此, 被观察的对象a就变成了中间类 NSKVONotifying_A
的对象了, 因而对对象a的被观察属性调用setter方法, 实际上就是调用中间类NSKVONotifying_A
中重写的setter, 从而激活KVO。
或者
// 在 `addObserver` 前后分别打印以下信息 object_getClass(self.a); // a的类, addObserver 前后类名发生了改变 [self.a methodForSelector:@selector(setNum:)]; // %p 打印方法地址, addObserver 前后地址值不一样 // 然后在a的setNum方法调用处, 打断点, lldb分别查看上面得到的IMP地址 // 可见, addObserver后, 属性num的setter方法的IMP变成了 _NSSetIntValueAndNotify (lldb)p (IMP)0x10123456 // 查看addObserver前的IMP地址, 控制台输出 (ModuleName`-[A setNum:]) (lldb)p (IMP)0x10123567 // 查看addObserver后的IMP地址, 控制台输出 (Foundation`_NSSetIntValueAndNotify)
② 子类setter方法剖析
KVO 的键值观察通知依赖于 NSObject 的两个方法:
willChangeValueForKey:
和 didChangeValueForKey:
在set属性的前后分别调用2个方法;
被观察属性发生改变之前, willChangeValueForKey:
被调用, 通知系统该 keyPath 的属性值即将变更;
改变发生后, didChangeValueForKey:
被调用, 通知系统该 keyPath 的属性值已经变更;
之后, observeValueForKey:ofObject:change:context:
也会被调用。(在didChangeValueForKey:
中被触发)
重写观察属性的 setter 方法, 这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; // KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO在调用存取方法之后总调用
}
图示
未使用KVO监听的对象
image使用了KVO监听的对象
imageKVO 注意点
KVO 触发条件
需调用对象属性的 setter
或使用 KVC
赋值, 才能触发 KVO。
使用KVC赋值
如果使用了KVC:
若有访问器方法, 运行时会在 访问器方法 中调用 will/didChangeValueForKey:
方法, 从而触发KVO
若无访问器方法, 运行时会在 setValue:forKey
方法中调用 will/didChangeValueForKey:
方法, 从而触发KVO
成员变量访问 ->
如果使用->直接对成员变量进行赋值, 不能触发 KVO
_psn->_hobby = @"tennis"; // 未调用setter, 不能触发KVO
多个监听, 来源判断
如果有多个监听, 必须对触发回调函数的来源进行判断
if (object == _psn && [keyPath isEqualToString:@"name"]) {}
未处理的KVO提交给父类处理
如果被观察者的类还有父类, 并且其父类对象也绑定了其他KVO, 则当前类可能无法捕捉到这个KVO, 此时应该传给super处理。
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
移除观察者
不需要监听时, 记得在观察者的 - (void)dealloc;
方法中移除KVO, 以防止内存泄漏
避免多次移除观察者导致crash
同一个 keypath 只能进行一次 removeObserver, 多次移除会导致程序crash, 这种情况常出现在父类和子类同时注册了同一个KVO, 销毁时会移除两次。
这种情况context字段不应传nil, 在父类和子类中定义各自唯一的context, 利用context来标识出KVO是父类or子类注册的
KVO 和 多线程
KVO 的响应 和 KVO 观察的值变化 是在一个线程上的, 所以, 大多数时候, 不要把KVO与多线程混合起来。
除非能够保证所有的观察者都能线程安全的处理KVO
面试题★★:
KVO 与 KVC 的不同?
KVO(Key-Value Observing 键值监听), 它提供一种机制, 当指定的对象的属性被修改后, 对象就会接受到通知, 前提是执行了setter方法、或者使用KVC赋值。
KVC(Key-Value Coding 键值编码), 使用字符串(键)访问一个对象实例变量的机制。而不是通过调用Setter、Getter方法等显式的存取方式去访问。
KVO 和 NSNotification的区别?
两者都是一对多。
NSNotification
比 KVO 多了发送通知的一步, 需要 notificationCenter
来做为中间交互。
NSNotification
的优点是监听范围广, 监听不局限于属性的变化, 还可以对如键盘弹出收起、程序进入前后台等各种状态变化进行监听。
KVO 只需要设置观察者->处理属性变化, 至于中间通知这一环, 使用runtime机制隐藏实现。
KVO、NSNotification 与 delegate 的异同及优缺点, 采用哪种方式更好?
-
相同点
delegate、KVO 和 NSNotification 的作用都是类与类之间的通信。 -
不同点
KVO 和 NSNotification 都是负责发送接收通知, 剩下的事情由系统处理, 所以不用返回值;delegate 则需要通信的对象通过代理联系;
delegate 只是一对一, 而 KVO 和 NSNotification 可以一对多。
-
简洁
KVO 相比 notification、delegate 等代码更简洁, 并且能够提供观察属性的最新值以及原始值;
但是相应的在创建子类、重写方法等等方面的内存消耗是很大的。
所以对于两个类之间的通信, 我们可以根据实际开发的环境采用不同的方法, 使得开发的项目更加简洁实用。 -
不观察0开销
另外, 由于KVO这种 继承方式的注入 是在运行时而不是编译时实现的, 如果给定的实例没有观察者, 那么KVO不会有任何开销, 因为此时根本就没有KVO代码存在。
但是即使没有观察者,delegate
和NSNotification
还是得工作, 这也是KVO此处零开销观察的优势。
如何手动触发KVO?
手动调用 willChangeValueForKey:
和 didChangeValueForKey:
。
实际上是 didChangeValueForKey:
中调用了 observer 的 observeValueForKey:ofObject:change:context:
理论上不需要调用 willChangeValueForKey:
, 但是 didChangeValueForKey:
内部会判断之前是否调用了 willChangeValueForKey:
, 如果没有调用就会失效。
直接修改成员变量会触发KVO么?
不会触发KVO
因为没有调用setter方法, KVO是依赖于setter的, 是把setter的实现替换掉
只有调用setter才会触发KVO