KVO
的全称是Key-Value Observing
,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,容许一个对象观察/监听另外一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO
的监听方法来通知观察者。KVO
是在MVC
应用程序中的各层之间进行通讯的一种特别有用的技术。KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。KVO
能够监听单个属性的变化,也能够监听集合对象的变化。监听集合对象变化时,须要经过KVC
的mutableArrayValueForKey:
等可变代理方法得到集合代理对象,当代理对象的内部对象发生改变时,会触发KVO
的监听方法。集合对象包含NSArray
和NSSet
。KVO
和KVC
有着密切的关系,若是想要深刻了解KVO
,建议先学习KVC
。 KVO
使用三部曲:添加/注册KVO
监听、实现监听方法以接收属性改变通知、 移除KVO
监听。html
addObserver:forKeyPath:options:context:
给被观察对象添加观察者;observeValueForKeyPath:ofObject:change:context:
方法以接收属性改变的通知消息;removeObserver:forKeyPath:
方法将观察者移除。须要注意的是,至少须要在观察者销毁以前,调用此方法,不然可能会致使Crash
。/* ** target: 被观察对象 ** observer:观察者对象 ** keyPath: 被观察对象的属性的关键路径,不能为nil ** options: 观察的配置选项,包括观察的内容(枚举类型): NSKeyValueObservingOptionNew:观察新值 NSKeyValueObservingOptionOld:观察旧值 NSKeyValueObservingOptionInitial:观察初始值,若是想在注册观察者后,当即接收一次回调,能够加入该枚举值 NSKeyValueObservingOptionPrior:分别在值改变先后触发方法(即一次修改有两次触发) ** context: 能够传入任意数据(任意类型的对象或者C指针),在监听方法中能够接收到这个数据,是KVO中的一种传值方式 若是传的是一个对象,必须在移除观察以前持有它的强引用,不然在监听方法中访问context就可能致使Crash */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
复制代码
若是对象被注册成为观察者,则该对象必须能响应如下监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。若是没有实现就会致使Crash
。git
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/* ** keyPath:被观察对象的属性的关键路径 ** object: 被观察对象 ** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回 key为 NSKeyValueChangeKey 枚举类型 { 1.NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key) { 对应枚举类型 NSKeyValueChange typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }; 若是是对被观察对象属性(包括集合)进行赋值操做,kind 字段的值为 NSKeyValueChangeSetting 若是被观察的是集合对象,且进行的是(插入、删除、替换)操做,则会根据集合对象的操做方式来设置 kind 字段的值 插入:NSKeyValueChangeInsertion 删除:NSKeyValueChangeRemoval 替换:NSKeyValueChangeReplacement } 2.NSKeyValueChangeNewKey: 存储新值(若是options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key) 3.NSKeyValueChangeOldKey: 存储旧值(若是options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key) 4.NSKeyValueChangeIndexesKey:若是被观察的是集合对象,change字典中就会包含这个key, 这个key的value是一个NSIndexSet对象,包含更改关系中的索引 5.NSKeyValueChangeNotificationIsPriorKey:若是options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。 这个key对应的value是NSNumber包装的YES,咱们能够这样来判断是否是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES] } ** context:注册方法中传入的context */
}
复制代码
在调用注册方法后,KVO
并不会对观察者进行强引用,因此须要注意观察者的生命周期。至少须要在观察者销毁以前,调用如下方法移除观察者,不然若是在观察者被释放后,再次触发KVO
监听方法就会致使Crash
。github
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
复制代码
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.name= @"张三";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"keyPath:%@",keyPath);
NSLog(@"object:%@",object);
NSLog(@"change:%@",change);
NSLog(@"context:%@",context);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}
复制代码
keyPath:name
object:<HTPerson: 0x600003ae4340>
change:{ kind = 1; new = "\U70b9\U51fb"; old = ""; }
context:(null)编程
KVO
主要用来作键值观察操做,想要一个值发生改变后通知另外一个对象,则用KVO
实现最为合适。斯坦福大学的iOS
教程中有一个很经典的案例,经过KVO
在Model
和Controller
之间进行通讯。如图所示: 数组
KVO
触发分为自动触发和手动触发两种方式。安全
① 若是是监听对象特定属性值的改变,经过如下方式改变属性值会触发KVO
:网络
setter
方法KVC
的setValue:forKey:
方法KVC
的setValue:forKeyPath:
方法② 若是是监听集合对象的改变,须要经过KVC
的mutableArrayValueForKey:
等方法得到代理对象,并使用代理对象进行操做,当代理对象的内部对象发生改变时,会触发KVO
。集合对象包含NSArray
和NSSet
。app
① 普通对象属性或是成员变量使用:框架
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
复制代码
② NSArray
对象使用:ide
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
复制代码
③ NSSet
对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
复制代码
observationInfo
属性是NSKeyValueObserving.h
文件中系统经过分类给NSObject
添加的属性,因此全部继承于NSObject
的对象都含有该属性;observationInfo
属性查看被观察对象的所有观察信息,包括observer
、keyPath
、options
、context
等。@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
复制代码
注册方法addObserver:forKeyPath:options:context:
中的context
能够传入任意数据,而且能够在监听方法中接收到这个数据。
context
做用:标签-区分,能够更精确的肯定被观察对象属性,用于继承、 多监听;也能够用来传值。
KVO
只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:
,咱们一般状况下能够在注册方法中指定context
为NULL
,并在监听方法中经过object
和keyPath
来判断触发KVO
的来源。
可是若是存在继承的状况,好比如今有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对 account 对象的 balance 属性进行观察。问题:
① 当 balance 发生改变时,应该由谁来处理呢?
② 若是都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是本身的事务仍是子类对象的事务呢?
这时候经过使用context
就能够很好地解决这个问题,在注册方法中为context
设置一个独一无二的值,而后在监听方法中对context
值进行检验便可。
苹果的推荐用法:用context
来精确的肯定被观察对象属性,使用惟一命名的静态变量的地址做为context
的值。能够为整个类设置一个context
,而后在监听方法中经过object
和keyPath
来肯定被观察属性,这样存在继承的状况就能够经过context
来判断;也能够为每一个被观察对象属性设置不一样的context
,这样使用context
就能够精确的肯定被观察对象属性。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
复制代码
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
复制代码
context
优势:嵌套少、性能高、更安全、扩展性强。context
注意点:context
就可能致使Crash
;NULL
而不该该传nil
。KVO
能够监听单个属性的变化,也能够监听集合对象的变化。监听集合对象变化时,须要经过KVC
的mutableArrayValueForKey:
等方法得到代理对象,当代理对象的内部对象发生改变时,会触发KVO
的监听方法。集合对象包含NSArray
和NSSet
。 (注意:若是直接对集合对象进行操做改变,不会触发KVO
。)
示例代码及输出以下:
观察者 viewController 对被观察对象 person 的 mArray 属性进行监听。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.mArray = [NSMutableArray arrayWithCapacity:5];
[self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person.mArray addObject:@"2"]; //若是直接对数组进行操做,不会触发KVO
NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
[array addObject:@"1"];
[array replaceObjectAtIndex:0 withObject:@"2"];
[array removeObjectAtIndex:0];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/* change 字典的值为: { indexes:对应的值为数组操做的详细信息,包括索引等 kind: 对应的值为数组操做的方式: 2:表明插入操做 3:表明删除操做 4:表明替换操做 typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }; new/old:若是是插入操做,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew) 若是是删除操做,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld) 若是是替换操做,则字典中new和old字段均可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( 1 ); } */
NSLog(@"%@",change);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"mArray"];
}
复制代码
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = (2); }
能够在被观察对象的类中重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法来控制KVO
的自动触发。
若是咱们只容许外界观察 person 的 name 属性,能够在 Person 类以下操做。这样外界就只能观察 name 属性,即便外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发KVO
。
// 返回值表明允不容许触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = YES;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
复制代码
也能够实现+ (BOOL)automaticallyNotifiesObserversOf<Key>
方法来单一控制属性的KVO
自动触发,<Key>
为属性名(首字母大写)。
+ (BOOL)automaticallyNotifiesObserversOfName
{
return NO;
}
复制代码
注意:
- 第一个方法的优先级高于第二个方法。若是实现了
automaticallyNotifiesObserversForKey:
方法,并对<Key>
作了处理,则系统就不会再调用该<Key>
的automaticallyNotifiesObserversOf<Key>
方法。options
指定的NSKeyValueObservingOptionInitial
触发的KVO
通知,是没法被automaticallyNotifiesObserversForKey:
阻止的。
使用场景:
KVO
监听成员变量值的改变; 因为KVO
的本质,重写setter
方法来达到能够通知全部观察者对象的目的,因此只有经过setter
方法或KVC
方法去修改属性变量值的时候,才会触发KVO
,直接修改为员变量不会触发KVO
。
当咱们要使用KVO
监听成员变量值改变的时候,能够经过在为成员变量赋值的先后手动调用willChangeValueForKey:
和didChangeValueForKey:
两个方法来手动触发KVO
,如:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"age"];
self.person->_age = 18;
[self.person didChangeValueForKey:@"age"];
}
复制代码
NSKeyValueObservingOptionPrior
(分别在值改变先后触发方法,即一次修改有两次触发)的两次触发分别在willChangeValueForKey:
和didChangeValueForKey:
的时候进行的。
若是注册方法中options
传入NSKeyValueObservingOptionPrior
,那么能够经过只调用willChangeValueForKey:
来触发改变前的那次KVO
,能够用于在属性值即将更改前作一些操做。
有时候咱们可能会有这样的需求,KVO
监听的属性值修改先后相等的时候,不触发KVO
的监听方法,能够结合KVO
的自动触发控制和手动触发来实现。
例如:对 person 对象的 name 属性注册了KVO
监听,咱们但愿在对 name 属性赋值时作一个判断,若是新值和旧值相等,则不触发KVO
,能够在 Person 类中以下这样实现,将 name 属性值改变的KVO
触发方式由自动触发改成手动触发。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = YES;
if ([key isEqualToString:@"name"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)setName:(NSString *)name
{
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
复制代码
有些状况下咱们想手动观察集合属性,下面以观察数组为例。
关键方法:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
复制代码
须要注意的是,根据KVC
的NSMutableArray 搜索模式
:
传送门:iOS - 关于 KVC 的一些总结
KVO
。如 插入方法:insertObject:in<Key>AtIndex:
或insert<Key>:atIndexes:
删除方法:removeObjectFrom<Key>AtIndex:
或remove<Key>AtIndexes:
KVO
会把它当成先删除后添加,即会触发两次KVO
。第一次触发的KVO
中change
字典的old
键的值为替换前的元素,第二次触发的KVO
中change
字典的new
键的值为替换后的元素,前提条件是注册方法中的options
传入对应的枚举值。KVO
,而且change
字典会同时包含new
和old
,前提条件是注册方法中的options
传入对应的枚举值。 替换方法:replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
示例代码以下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"mArray"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray insertObjects:array atIndexes:indexes];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray removeObjectsAtIndexes:indexes];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray replaceObjectsAtIndexes:indexes withObjects:array];
[self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}
复制代码
有些状况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
好比咱们想要对 Download 类中的 downloadProgress 属性进行KVO
监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。如下有两种方法能够解决这个问题。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"writtenData",@"totalData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
复制代码
keyPathsForValuesAffecting<Key>
的类方法,<Key>
是依赖于其余值的属性名(首字母大写):+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}
复制代码
注意: 以上两个方法能够同时存在,且都会调用,可是最终结果会以
keyPathsForValuesAffectingValueForKey:
为准。
以上方法在观察集合属性时就无论用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你但愿 Department 类有一个 totalSalary 属性来计算全部员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于全部 Employee 实例对象的 salary 属性。如下有两种方法能够解决这个问题。
KVO
将 parent(好比 Department )做为全部 children(好比 Employee )相关属性的观察者。你必须在把 child 添加或删除到 parent 时把 parent 做为 child 的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:
方法中咱们能够针对被依赖项的变动来更新依赖项的值:#import "Department.h"
static void *totalSalaryContext = &totalSalaryContext;
@interface Department ()
@property (nonatomic,strong)NSArray<Employee *> *employees;
@property (nonatomic,strong)NSNumber *totalSalary;
@end
@implementation Department
- (instancetype)initWithEmployees:(NSArray *)employees
{
self = [super init];
if (self) {
self.employees = [employees copy];
for (Employee *em in self.employees) {
[em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
}
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)setTotalSalary:(NSNumber *)totalSalary
{
if (_totalSalary != totalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = totalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (void)dealloc
{
for (Employee *em in self.employees) {
[em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
}
}
@end
复制代码
iOS
中观察者模式的另外一种实现方式:通知 (NSNotification
) 。KVO
注册方法后,KVO
并不会对观察者进行强引用,因此须要注意观察者的生命周期。至少须要在观察者销毁以前,调用KVO
移除方法移除观察者,不然若是在观察者被释放后,再次触发KVO
监听方法就会致使Crash
。KVO
的注册方法和移除方法应该是成对的,若是重复调用移除方法,就会抛出异常NSRangeException
并致使程序Crash
。init
或者viewDidLoad
的时候)注册为观察者,在释放过程当中(dealloc
时)调用移除方法,这样能够保证它们是成对出现的,是一种比较理想的使用方式。 有时候咱们难以免屡次注册和移除相同的KVO
,或者移除了一个未注册的观察者,从而产生可能会致使Crash
的风险。
三种解决方案:黑科技防止屡次添加删除KVO出现的问题
@try @catch
(只能针对删除屡次KVO
的状况下) 给NSObject
增长一个分类,而后利用Runtime API
交换系统的removeObserver
方法,在里面添加@try @catch
。observationInfo
里私有属性Crash
。因此KVO
三部曲缺一不可。keyPath
传入的是一个字符串,为避免写错,可使用NSStringFromSelector(@selector(propertyName))
,将属性的getter
方法SEL
转换成字符串,在编译阶段对keyPath
进行检验。context
传的是一个对象,必须在移除观察以前持有它的强引用,不然在监听方法中访问context
就可能致使Crash
。KVC
的mutableArrayValueForKey:
等方法得到代理对象,并使用代理对象进行操做,当代理对象的内部对象发生改变时,会触发KVO
。若是直接对集合对象进行操做改变,不会触发KVO
。context
或者object
、keyPath
调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
。Key-Value Observing Implementation Details
- Automatic key-value observing is implemented using a technique called isa-swizzling.
- The
isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.- When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
- You should never rely on the
isa
pointer to determine class membership. Instead, you should use theclass
method to determine the class of an object instance.
以上是苹果官方对KVO
实现的解释,只说明了KVO
是使用isa-swizzling
技术来实现的,并无作过多介绍。
苹果使用了isa
混写技术(isa-swizzling
)来实现KVO
。当咱们调用了addObserver:forKeyPath:options:context:
方法,为instance
被观察对象添加KVO
监听后,系统会在运行时利用Runtime API
动态建立instance
对象所属类A
的子类NSKVONotifying_A
,而且让instance
对象的isa
指向这个全新的子类,并重写原类A
的被观察属性的setter
方法来达到能够通知全部观察者对象的目的。
这个子类的isa
指针指向它本身的meta-class
对象,而不是原类的meta-class
对象。
重写的setter
方法的SEL
对应的IMP
为Foundation
中的_NSSetXXXValueAndNotify
函数(XXX
为Key
的数据类型),当被观察对象的属性发送改变时,会_NSSetXXXValueAndNotify
函数,这个函数中会调用:
willChangeValueForKey:
方法setter
方法didChangeValueForKey:
方法(内部会触发监听器即观察对象observer
的监听方法:observeValueForKeyPath:ofObject:change:context:
) 在移除KVO
监听后,被观察对象的isa
会指回原类A
,可是NSKVONotifying_A
类并无销毁,还保存在内存中。
NSKVONotifying_A
除了重写了setter
方法,还重写了class
、dealloc
、_isKVOA
这三个方法(可使用runtime
的class_copyMethodList
函数打印方法列表得到),其中:
class
:class
方法中返回的是父类的class
对象,目的是为了避免让外界知道KVO
动态生成类的存在;dealloc
:释放KVO
使用过程当中产生的东西;_isKVOA
:用来标志它是一个KVO
的类。KVO
监听、实现监听方法以接收属性改变通知、 移除KVO
监听,缺一不可;context
是经过void *
指针;-observeValueForKeyPath:ofObject:change:context:
方法,比较麻烦;if
进行判断。FBKVOController
是 Facebook 开源的一个基于系统KVO
实现的框架。支持Objective-C
和Swift
语言。
GitHub:github.com/facebook/KV…
KVO
的三个步骤;KVO
与事件发生处的代码上下文相同,不须要跨方法传参数;block
和SEL
自定义操做对NSKeyValueObserving
回调的处理支持;keyPath
会对应一个block
或者SEL
,不须要使用if
判断keyPath
;FBKVOController
实现了观察者和被观察者的角色反转,系统的KVO
是被观察者添加观察者,而FBKVO
实现了观察者主动去添加被观察者,实现了角色上的反转,使用比较方便。
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update clock view with new value
clockView.date = change[NSKeyValueChangeNewKey];
}];
// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
复制代码
如何优雅地使用KVO(简书)
iOS - FBKVOController 实现原理(简书)
Key-Value Observing Programming Guide(苹果官方文档)
iOS - 关于 KVC 的一些总结(掘金)
KVO原理分析及使用进阶(简书)
iOS开发 - 黑科技防止屡次添加删除KVO出现的问题(简书)
谈谈 KVO(简书)
GitHub/facebook/KVOController(GitHub)
如何优雅地使用KVO(简书)
iOS - FBKVOController 实现原理(简书)