15:KVO (上) —— 使用篇
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种:NSKeyValueChangeInsertion
NSKeyValueChangeRemoval
NSKeyValueChangeReplacement
下标集合是
NSIndexSet
类型 -
示例:某个类的
removeTransactionsAtIndexes:
方法里进行移除,仍然是上下夹写- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; // 对指定的那些下标项,进行移除操作 [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; }
手动调用和自动调用的本质区别
-
自动调用,设置
image.pngwatchpoint
,在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
,所以在回调方法里也要做处理