KVO 3个步骤 (自动调用)
对对象自动调用
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 注册观察者
[person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:NULL];
// 对象中被观察的属性发生变化
person.name = @"Mark"; // 点语法
[person setName:@"Dash"]; // setter
[person setValue:nil forKey:@"name"]; // KVC
}
// 2. 监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
MDNSLog(@"keyPath: %@", keyPath);
MDNSLog(@"object: %@", object);
MDNSLog(@"change: %@", change);
}
// 3. 移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
}
@end
控制台打印↓
keyPath: name
object: <MDPerson: 0x600001561340>
change: {
kind = 1;
new = Mark;
old = "<null>";
}
keyPath: name
object: <MDPerson: 0x600001561340>
change: {
kind = 1;
new = Dash;
old = Mark;
}
keyPath: name
object: <MDPerson: 0x600001561340>
change: {
kind = 1;
new = "<null>";
old = Dash;
}
-
三个步骤:
-
注册观察者。 person对象说:
self作为观察者,观察我里面的name属性,一旦有变化,我需要了解new新值和old旧值,context是一个 tag(待会详细说) -
监听回调。 参数有:发生变化的属性名
name,这个属性所属的对象<MDPerson: 0x600001561340>,发生了什么变化change: {kind = 1;new = Mark;old = "<null>";} -
移除观察者。 在观察者被销毁之前,要移除监听,否则会出问题。假设观察者被销毁,
person仍然存在(譬如单例对象),如果name发生改变则会触发监听回调,系统会因为找不到观察者而崩溃(相当于野指针)。但也要注意,移除不存在的观察者,系统会崩溃
-
对数组/集合自动调用
-
三个步骤 同上
-
set值的时候有区别
[[person mutableArrayValueForKey:@"hobbies"] addObject:@"sleep"]; [[person mutableArrayValueForKey:@"hobbies"] replaceObjectAtIndex:0 withObject:@"run"]; [[person mutableArrayValueForKey:@"hobbies"] removeObjectAtIndex:0]; // 监听回调 MDNSLog(@"%@", change);控制台打印↓
{ indexes = "<_NSCachedIndexSet: 0x600001c1e860>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( sleep ); } { indexes = "<_NSCachedIndexSet: 0x600001c1e860>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = ( run ); old = ( sleep ); } { indexes = "<_NSCachedIndexSet: 0x600001c1e860>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = ( run ); }
打印 change 时,里面的 kind 是什么?
-
kind属于NSKeyValueChangeKindKey- 1 对象的
所有变化 - 2 数组/集合的
新增元素 - 3 数组/集合的
移除元素 - 4 数组/集合的
替换元素
typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }; - 1 对象的
context 有什么用?
-
官方文档描述了这样一个问题:
You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
你有可能给
context传NULL,然后根据keyPath来定位想要的监听。但这种方法有可能出现问题:如果object的父类也在观察这个keyPath。
父类和子类同时观察同一个 keyPath 引起问题
-
父类
.m@implementation MDPerson - (instancetype)init { self = [super init]; if (self) { // 注册观察者 [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL]; } return self; } // 父类的监听回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // 识别 KeyPath if ([keyPath isEqualToString:@"name"]) { MDNSLog(@"(Person) new name is: %@", change[@"new"]); } } @end -
子类
.m@implementation MDStudent - (instancetype)init { self = [super init]; if (self) { // 注册观察者 [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL]; } return self; } // 子类的监听回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // 识别 KeyPath if ([keyPath isEqualToString:@"name"]) { MDNSLog(@"(Student) new name is: %@", change[@"new"]); } } @end -
被观察的属性发生变化
// 创建对象 MDStudent *student = [[MDStudent alloc] init]; // 属性发生变化 student.name = @"Mark"; -
控制台打印↓
(Student) new name is: Mark (Student) new name is: Mark -
问题:不走父类
MDPerson的监听回调,走了2次子类MDStudent的监听回调 -
原因:首先,
父类的注册观察者和子类的注册观察者,这2行代码(在这个情况)本质做的是同一件事情,等价于== 写2次父类的注册观察者或者 写2次子类的注册观察者;其次,子类重写了监听回调,自然就不走父类的了。
解决问题
-
注册时,传
context;监听时,识别context -
效果:
父类的注册观察者和子类的注册观察者,这2行代码不一样了,子类的传有context;子类监听回调识别context,前一次识别成功,后一次识别失败(context为NULL) 抛给父类处理。// 定义一个 context static void *StudentContext = &StudentContext;// 注册观察者,传 context [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:StudentContext];// 监听回调里面,要识别 context if (context == StudentContext) { MDNSLog(@"(Student) new name is: %@", change[@"new"]); } else { // 任何未识别的 context 原则上要抛给父类处理 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }控制台打印↓
(Student) new name is: Mark (Person) new name is: Mark
context 通常用法
- 除了用来解决以上问题,
context最简单的用法就是作为TAG将监听定位到某个对象
PS:以上问题只能用
context解决,而下面的代码却有其他一些替代方案。意味着有些被广泛应用的特性其实是为了解决某个罕见问题,而我们却毫无察觉╮(╯▽╰)╭。
-
简单区分
person对象和p2对象↓// 在外部定义 context static void *personNameContext = &personNameContext; static void *p2NameContext = &p2NameContext;// 注册观察者时使用 context [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:personNameContext]; [p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:p2NameContext];// 监听回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == personNameContext) { MDNSLog(@"new name of person is: %@", change[@"new"]); } else if (context == p2NameContext) { MDNSLog(@"new name of p2 is: %@", change[@"new"]); } }
手动调用 KVO
对对象手动调用
-
以上,在属性发生变化时,KVO的监听回调是
自动触发的;我们也可以通过一个开关来手动触发 -
Person.m添加下面代码,则person.name = @"Mark"不触发监听回调+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { BOOL automatic = NO; if ([key isEqualToString:@"name"]) { // 仅对该key禁用系统自动通知,若要禁用该整个类的KVO则直接返回NO; automatic = NO; } else { // 识别不到的 key,抛给父类处理 automatic = [super automaticallyNotifiesObserversForKey:key]; } return automatic; } -
想让哪行赋值代码触发监听回调,就在那行代码的上下夹写
willChangeValueForKey:和didChangeValueForKey:(可能在touchesBegan:withEvent:里,可能在类的setter里..)[student willChangeValueForKey:@"name"]; student.name = @"Mark"; [student didChangeValueForKey:@"name"];
对数组/集合手动调用
-
官方文档还提及了
to-many类型的情况:In the case of an ordered to-many relationship, you must specify not only the key that changed, but also the type of change and the indexes of the objects involved. The type of change is an NSKeyValueChange that specifies NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, or NSKeyValueChangeReplacement. The indexes of the affected objects are passed as an NSIndexSet object.
数组或集合的情况,除了指定发生变化的
key,还要指定改变类型以及下标集合。改变类型是
NSKeyValueChange,有3种:NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement下标集合是
NSIndexSet类型 -
示例:某个类的
removeTransactionsAtIndexes:方法里进行移除,仍然是上下夹写- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; // 对指定的那些下标项,进行移除操作 [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; }
手动调用和自动调用的本质区别
-
自动调用,设置
watchpoint,在setter被调用时,在汇编里看到系统已经自动给class_getMethodImplementation夹写了willChange和didChange
C = A + B,如何在 A或B 发生变化时,能监听到C的值
复合路径的使用
-
KVO建立在KVC上,本质是监听setter- 注册观察A - setA - 监听回调得到A,成功
- 注册观察C - setA - 监听回调得到C,失败
- 注册观察C - 告诉系统C会被A影响 - setA - 监听回调得到C,成功,看下面实现
-
Person.h@interface MDPerson : NSObject // 属性 @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @end -
Person.m-
重写
被影响者(name) 的getter方法 (和KVC流程不完全对标,尝试过只能是getName或name) -
重写
keyPathsForValuesAffectingValueForKey:设置影响者-被影响者。Person类有3个属性,运行时 该方法会进来3次,记录每个属性的影响者(key为firstName或lastName时,返回的keyPaths为空)
@implementation MDPerson - (NSString *)name { return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName]; } + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"name"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } @end -
-
ViewController.m,这之前差不多,只不过发生改变的是影响者@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 创建对象 MDPerson *person = [[MDPerson alloc] init]; // 注册观察者 (被影响者) [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL]; // 影响者发生改变 person.firstName = @"Mark"; person.lastName = @"Dash"; } // 监听回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { MDNSLog(@"new name of person is: %@", change[@"new"]); } @end控制台打印↓
new name of person is: Mark (null) new name of person is: Mark Dash
意外情况
-
name的getter方法被重写,导致外界的person.name无法获取到_name,如果想获取就要另外写一个方法return _name -
即使这样,调用
person.name = @"Mark"时,回调方法的change依然是firstName lastName而不是_name,所以在回调方法里也要做处理