目录
1. 什么是KVO?
KVO的全称是Key-Value Observing,即键值监听或键值观察,用于监听某个对象属性值的改变
KVO是苹果提供的一套事件通知机制(其声明全部在Foundation框架中的NSKeyValueObserving.h
里),允许一个对象监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者
因为KVO接口声明是@interface NSObject(NSKeyValueObserving)
,即给NSObject添加的分类Category,所以大多数对象都可以键值观察或键值监听
2. KVO的基本使用
KVO使用主要有以下三步:
-
添加/注册KVO监听:调用
addObserver:forKeyPath:options:context:
给被观察对象添加观察者- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- target:方法调用者为被观察对象
- observer:观察者对象
- keyPath:被观察者对象的关键路径,不能为nil
- options:配置观察内容的枚举选项,
- context:可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式。如果传的是一个对象,必须在移除观察之前持有这个对象的强引用,否则在监听方法中访问context就可能导致Crash
-
实现监听方法来接收属性改变通知:监听方法为
observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { }
- keyPath:被观察对象属性的关键路径
- object:被观察对象
- change:字典NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回,key为NSKeyValueChangeKey枚举类型
- NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)
对应枚举类型NSKeyValueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }
如果是对被观察对象属性(包括集合)进行赋值操作,
kind
字段的值为NSKeyValueChangeSetting
如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则会根据集合对象的操作方式来设置kind
字段的值:NSKeyValueChangeInsertion插入、NSKeyValueChangeRemoval删除、NSKeyValueChangeReplacement替换 - NSKeyValueChangeNewKey:存储新值(如果options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key)
- NSKeyValueChangeOldKey:存储旧值(如果options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key)
- NSKeyValueChangeIndexesKey:如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则change字典中就会包含这个key,这个key的value是一个
NSIndexSet
对象,包含更改关系中的索引 - NSKeyValueChangeNotificationIsPriorKey:如果options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。
这个key对应的value是NSNumber包装的YES,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES
- NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)
- context:注册方法中传入的context
-
移除KVO监听:当观察者不需要再监听时,调用
removeObserver:forKeyPath:
方法将观察者移除。需要注意的是,至少需要在观察者销毁之前,调用此方法,否则再次触发KVO监听方法就可能会导致Crash- (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* person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver: self forKeyPath: @"age" options: options context: NULL];
}
// 改变属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self.person setAge: 12];
self.person.age = 12;
}
// 监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
// 移除监听
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
使用KVO为person对象添加观察者为当前viewController,监听person对象的name属性值的改变,当点击当前页面使name值改变时,触发KVO的监听方法:
3. KVO的进阶使用
observationInfo属性
observationInfo属性是NSKeyValueObserving.h文件中系统通过分类给NSObject添加的属性,所以所有继承于NSObject的对象都含有该属性
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
可以通过observationInfo属性查看被观察对象的全部观察信息,包括observer、keyPath、options、context等
// 这里的 person 对象已被 KVO 监听
NSLog(@"%@", person.observationInfo);
context 的使用
注册方法addObserver:forKeyPath:options:context:
中的context可以传入任意数据,并且可以在监听方法中接收到这个数据
context
作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值
KVO只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:,我们通常情况下可以在注册方法中指定context为NULL
,并在监听方法中通过object
和keyPath
来判断触发KVO的来源
但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对 account 对象的 balance 属性进行观察(Observer)。问题:
- 当 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触发监听方法的方式
自动触发
默认使用以下方式改变被监听属性的值会自动触发 KVO 方法:
- 点语法
- setter 方法
- KVC 的
setValue:forKey:
和setValue:forKey:
方法
如果是监听集合对象的改变,需要通过KVC的mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。集合对象包含NSArray
和NSSet
手动触发
要想手动触发KVO,需要修改类方法automaticallyNotifiesObserversForKey:
,下面的逻辑让我们精准施策,选择对哪些属性是自动,哪些属性是手动:
// 默认返回触发返回YES,即如果不手动调用合适的方法的话,就不会触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
if ([key isEqualToString: @"age"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey: key];
}
return automatic;
}
也可以实现遵循命名规则为+ (BOOL)automaticallyNotifiesObserversOf<Key>
的方法来单一控制属性的KVO自动触发,<Key>
为属性名(首字母大写):
+ (BOOL)automaticallyNotifiesObserversOfAge {
return NO;
}
且该方法的优先级高于上面的方法;options指定的NSKeyValueObservingOptionInitial触发的KVO通知,是无法被automaticallyNotifiesObserversForKey:阻止的
普通对象属性或是成员变量使用:
- (void)setAge:(int)age {
if (_age != age) {
[self willChangeValueForKey: @"age"];
_age = age;
[self didChangeValueForKey: @"age"];
}
}
对于集合对象,必须指定更改的类型和所涉及对象的索引:
- (void)removeBooksAtIndexes:(NSIndexSet *)indexes {
[self willChange: NSKeyValueChangeRemoval
valuesAtIndexes: indexes forKey: @"books"];
// Remove the book objects at the specified indexes.
[self didChange: NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey: @"books"];
}
更改的类型是NSKeyValueChange
:
NSKeyValueObservingOptionPrior
(分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在willChangeValueForKey:和didChangeValueForKey:的时候进行的。如果注册方法中options传入NSKeyValueObservingOptionPrior,那么可以通过只调用willChangeValueForKey:来触发改变前的那次KVO,可以用于在属性值即将更改前做一些操作
下面以观察数组为例。
关键方法:
- (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"];
}
实际自动触发调用的就是这些函数,手动触发不会动态生成子类,下面的KVO 本质分析会提到
KVO新旧值相等时不触发
被 KVO 监听的属性修改前后值相等时,也会触发监听方法:
self.person.age = 12;
self.person.age = 12;
/*
change: {
kind = 1;
new = 12;
old = 12;
}
*/
有时会认为这样的值没必要监听,就可通过重写automaticallyNotifiesObserversForKey:
和setter
方法,当属性被修改前后值相等时,不触发 KVO:
// 首先关闭手动触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL isOpen = YES;
if ([key isEqualToString: @"age"]) {
isOpen = NO;
}
return isOpen;
}
// 修改前后值不相等,不去触发 KVO
- (void)setAge:(int)age {
//手动设置KVO
if (_age != age) {
[self willChangeValueForKey: @"age"];
_age = age;
[self didChangeValueForKey: @"age"];
}
}
KVO的从属关系
一对一关系
有时,一个属性的改变依赖于其他的一个或多个属性的改变
比如,对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于wirteData
和totalData
属性的改变
- (NSString *)downloadProgress {
return [NSString stringWithFormat: @"%@ %@", self.writeData, self.totalData];
}
法一: 重写类方法keyPathsForValuesAffectingValueForKey
,来返回一个集合
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString: @"downloadProgress"]) {
NSArray* affectingKeys = @[@"writtenData", @"totalData"];
keyPaths = [keyPaths setByAddingObjectsFromArray: affectingKeys];
}
return keyPaths;
}
这里需要先对父类发送keyPathsForValuesAffectingValueForKey
消息,以免干扰父类中对此方法的重写
法二: 实现一个遵循命名规则为keyPathsForValuesAffecting<Key>
的类方法,<Key>
是依赖于其他值的属性名(首字母大写),针对某个属性:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress {
return [NSSet setWithObjects: @"writeData", @"totalData", nil];
}
以上两个方法可以同时存在,且都会调用,但是最终结果会以
keyPathsForValuesAffectingValueForKey:
为准
一对多关系
以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性
你希望 Department 类有一个totalSalary
属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题:
法一: 用KVO将 Department 作为所有 employee 相关属性的观察者,在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.totalSalary = [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
法二: 使用通知中心NSNotification
4. KVO使用注意
-
至少需要在观察者销毁之前,调用KVO移除方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash
-
我们在注册观察者的时候,要求传入的
keyPath
是字符串类型,如果我们拼写错误的话,编译器是不能帮我们检查出来的,所有最佳实践应该是使NSStringFromSelector(SEL aSelector)
,比如我们要观察tableView的contentSize属性,我们可以这样使用:NSStringFromSelector(@selector(contentSize))
将 getter 方法 SEL 转换成字符串,在编译阶段进行检验
-
有时候我们难以避免多次注册和移除相同的KVO,或者移除了一个未注册的观察者,从而产生可能会导致Crash的风险
三种解决方案:黑科技防止多次添加删除KVO出现的问题- 利用 @try @catch(只能针对删除多次KVO的情况下)
给NSObject增加一个分类,然后利用Runtime API交换系统的removeObserver方法,在里面添加@try @catch - 利用 模型数组 进行存储记录
- 利用 observationInfo 里私有属性
- 利用 @try @catch(只能针对删除多次KVO的情况下)
-
观察者对象所属类中必须实现监听方法,否则会 Crash
-
如果是监听集合对象的改变,需要通过KVC的
mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。如果直接对集合对象进行操作改变,不会触发KVO -
在观察者类的监听方法中,应该为无法识别的context、object或keyPath调用父类的实现
[super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
5. KVO本质原理分析
从上面的代码可以得知,只有对象被观察者监听了,该对象的属性值使用点语法或setter方法赋值时才会触发KVO,没有被监听的对象则不会触发KVO监听方法
这是为什么呢?
我们将person变量名改为person1,再初始化一个person2对象,去改变其age属性值,但不使用KVO为它添加观察者(未被监听),打断点使用lldb
打印出两个对象isa所指向的类:
可以得知,被KVO监听的对象person1的isa指向的类对象已经不是原本的Person类,而是一个Runtime运行时动态创建的新类NSKVONotifying_Person
(且是Person类的子类):
对象调用一个方法,实际就是给实例对象(调用者)发送一个消息,根据实例对象的isa指针找到类对象,而后再在类对象里找到方法的实现去调用。
当使用setter方法改变被监听的person1对象的属性值时,会找到NSKVONotifying_Person
类下面的setAge:(已被重写)进行调用,而此方法的实现已经不是简单地给实例变量赋值_age = age
,而是调用Foundation框架中的C函数_NSSetIntValueAndNotify()
,这个函数中会有调用KVO监听方法的代码段
伪代码
因为我们无法知道Foundation框架的源码,所以我们可以从结果推出一些伪代码,探索出KVO大概做了些什么事情:
// NSKVONotifying_Person.h
#import "Person.h"
@interface NSKVONotifying_Person : Person
@end
// NSKVONotifying_Person.m
#import "NSKVONotifying_Person.h"
@implementation NSKVONotifying_Person
- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify(void) {
[self willChangeValueForKey: @"age"];
[super setAge: age]; // _age = age
[self didChangeValueForKey: @"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 通知观察者(监听器),某某属性值发生了改变
[observer observeValueForKeyPath: key ofObject: self change: nil context: NULL];
// ...
}
@end
person1和person2两个对象的isa指向不同,即类对象不同,就会使找到的对应方法的实现不一样,所以才导致两种不同的结果(被KVO监听的对象指定属性值改变,触发监听方法,未被监听的对象属性值改变,不会触发监听方法)。
现在来验证以上分析是否正确:
保留伪代码下的类并编译运行
现在注释掉伪代码,保留类的声明和实现文件,运行项目,控制台会出现如下提示:
意为KVO创建NSKVONotifying_Person
类失败,因为自己已经强行手动创建了该类,所以无法在运行时动态生成此类解决方案就是,将此类不纳入待编译文件内:
在此处将其删掉即可,表示项目中只是存在这些代码,但不参与编译
也从侧面证明了确实有动态生成NSKVONotifying_Person类
对比添加监听前后实例对象的类对象
NSLog(@"person1添加KVO监听之前 - %@ 和person2: %@", object_getClass(self.person1), object_getClass(self.person2));
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];
NSLog(@"person1添加KVO监听之后 - %@ 和person2: %@", object_getClass(self.person1), object_getClass(self.person2));
运行结果表明被监听对象所属的类的确不是原来的类:
对比添加监听前后实例对象的方法实现
NSLog(@"person1添加KVO监听之前setAge:方法 - %p 和person2: %p", [self.person1 methodForSelector: @selector(setAge:)], [self.person2 methodForSelector: @selector(setAge:)]);
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];
NSLog(@"person1添加KVO监听之后setAge:方法 - %p 和person2: %p", [self.person1 methodForSelector: @selector(setAge:)], [self.person2 methodForSelector: @selector(setAge:)]);
打断点,根据地址使用lldb
将setAge:方法在控制台打印出来,运行结果表明被监听对象的方法实现跟监听前不一样,也得知了具体的方法声明:
这里如果修改的属性时double类型,实际调用的方法应该是_NSSetDoubleValueAndNotify
:
当然不同类型的属性会调用不同的C函数:
对比添加监听前后实例对象的类和元类
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];
NSLog(@"类对象 - %p %p",
object_getClass(self.person1), // self.person1.isa
object_getClass(self.person2)); // self.person2.isa
NSLog(@"元类对象 - %p %p",
object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
运行结果表明,person1的类(NSKVONotifying_Person)虽继承与person2的类(Person),但它们的元类各不相同,每个类对象都有各自的元类对象:
监听器监听方法的调用时机和顺序
在Person类中重写以下方法:
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age {
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey: key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey: key];
NSLog(@"didChangeValueForKey - end");
}
@end
运行结果表明当被监听的对象的属性改变时,会先调用willChangeValueForKey:
,再调用setter
相关方法,最后是调用didChangeValueForKey
,监听方法的确是在didChangeValueForKey:
方法里面调用的:
动态生成类重写的方法
对象在被KVO监听后,全新生成的NSKVONotifying_Person类里面,除重写了父类Person的setAge:
方法之外,还重写了class
、dealloc
、isKVOA
三个方法
用class方法打印被KVO监听后对象的类对象:
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];
// 访问isa指针
NSLog(@"%@ %@", object_getClass(self.person1), object_getClass(self.person2));
// 调用class方法
NSLog(@"%@ %@", [self.person1 class], [self.person2 class]);
从打印结果来看,class方法返回的不见得是真实的类对象:
可推出重写的class方法可能会是这样:
- (void)setAge:(int)age {
_NSSetIntVlaueAndNotify();
}
- (Class)class {
return [Person class];
}
- (void)dealloc {
// 一些收尾工作
}
- (BOOL)_isKVOA {
return YES;
}
可猜测这样重写的原因是,从我们开发者角度来看,这个新类NSKVONotifying_Person是需要被隐藏的,官方不希望它被暴露出来,屏蔽了内部实现
如果没有重写class方法,则会根据继承链往上找到NSObject基类的class方法:
// 伪代码
@implementation NSObject
- (Class)class {
return object_getClass(self);
}
@end
也会打印出真实的类对象,所以不重写class方法,就达不到隐藏新类的目的
打印新类的方法列表名称
写一个方法对指定类的方法名进行打印:
// 打印出某个类的所有方法名
- (void)printMethodNamesOfClass:(Class)cls {
unsigned int count;
// 获得方法数组
Method* methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString* methodNames = [NSMutableString string];
// 遍历方法数组
for (int i = 0; i < count; ++i) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString* methodName = NSStringFromSelector(method_getName(method));
//拼接方法名
[methodNames appendString: methodName];
[methodNames appendString: @", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@: %@", cls, methodNames);
}
调用:
[self printMethodNamesOfClass: object_getClass(self.person1)];
[self printMethodNamesOfClass: object_getClass(self.person2)];
可以得知动态生成的新类确实也重写了上面提到的方法