KVO的基本使用
基本使用
KVO:Key Value Observing(键值监听),用来监听某个对象属性值的改变。
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
@end
@interface KVOViewController ()
@property (nonatomic, strong) Person *person1; @property (nonatomic, strong) Person *person2;
@end
@implementation KVOViewController
- (void)viewDidLoad { [super viewDidLoad]; self.person1 = [[Person alloc] init]; self.person1.age = 10; self.person2 = [[Person alloc] init]; self.person2.age = 20; NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:options context:NULL]; }
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.person1.age = 11; self.person2.age = 21; }
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"%@对象的%@属性发生了改变:\n%@",object, keyPath,change); }
2022-11-15 20:08:33.563589+0800 OC对象的本质[81675:15955895] <Person: 0x6000007519c0>对象的age属性发生了改变: { kind = 1; new = 11; old = 10; }
|
注意:在不需要监听的时候需要移除。
- (void)dealloc { [self.person1 removeObserver:self forKeyPath:@"age"]; }
|


解释:
1、KVO是建立在KVC的基础之上的,即是说给成员变量赋值KVO是无法监听其变化的。
2、context意为上下文信息,我们平时用的时候一般传的NULL
,但是苹果官方建议的是把这个参数用起来会更安全、扩展性更强。

KVO其他细节
1、是否打开自动观察的开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key { return YES; // 默认是YES }
|
2、返回可能影响监听值的NSSet
// 当writtenData发生改变的时候,downloadProgress就会发生改变 + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"downloadProgress"]) { NSArray *affectingKeys = @[@"writtenData"]; // 只要affectingKeys数组里面的属性发生变化 都会触发downloadProgress的KVO keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; }
|
3、对可变数组的监听
self.person1.booksArr = [NSMutableArray array]; [self.person1 addObserver:self forKeyPath:@"booksArr" options:NSKeyValueObservingOptionNew context:NULL]; [[self.person1 mutableArrayValueForKey:@"booksArr"] addObjectsFromArray:@[@"Hello", @"World"]];
|
// 打印结果:

KVO的本质分析
从上面的例子咱们发现:两个不同的对象person1
和person2
,为什么person1
添加了KVO可以监听到属性值的改变?
看起来self.person1.age = 11
和 self.person2.age = 21
都是调用的setAge:
方法,为什么person1
就可以监听到属性值的改变了呢?我们可以大胆猜测一下person1
和person2
的setAge:
的具体实现肯定不一样了,也就是说person1
的isa
和person2
的isa
指向发生了变化,下面我们来验证一下我们的猜想。
验证一:
在person1
添加KVO前后分别打印person1
和person2
的class对象:
NSLog(@"person1添加监听之前:person1:%@ person2:%@",object_getClass(self.person1),object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1添加监听之后:person1:%@ person2:%@",object_getClass(self.person1),object_getClass(self.person2));
|
打印结果:
2022-11-15 20:21:21.161924+0800 OC对象的本质[81836:15966659] person1添加监听之前:person1:Person person2:Person 2022-11-15 20:21:21.162217+0800 OC对象的本质[81836:15966659] person1添加监听之后:person1:NSKVONotifying_Person person2:Person
|
如上:我们发现在person1
添加了KVO之后,person1
的isa
指向的是NSKVONotifying_Person
类,person2
的isa
指向的还是Person
类。
验证二:
在person1
添加KVO前后分别打印person1
和person2
的setAge:
方法的函数地址(IMP):
NSLog(@"person1添加监听之前:person1:%p person1:%p",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1添加监听之后:person1:%p person1:%p",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
|
打印结果:
2022-11-15 20:32:28.980113+0800 OC对象的本质[81977:15975768] person1添加监听之前:person1:0x102539e70 person1:0x102539e70 2022-11-15 20:32:28.980454+0800 OC对象的本质[81977:15975768] person1添加监听之后:person1:0x7fff207b1cfb person1:0x102539e70
|
如上:我们发现在person1
添加了KVO之后,person1
的IMP
跟person2
的IMP
不一样。
通过LLDB指令:
p (IMP) 0x7fff207b1cfb (IMP) $0 = 0x00007fff207b1cfb (Foundation`_NSSetIntValueAndNotify)
p (IMP) 0x102539e70 (IMP) $1 = 0x0000000102539e70 (OC对象的本质`-[Person setAge:] at Person.h:12)
|
如上:我们发现person1
的setAge:
方法其实是调用到了一个C函数:_NSSetIntValueAndNotify
。
通过上面的分析:
1、我们看到person1
添加了KVO之后,其isa
指针指向的是一个派生类NSKVONotifying_Person
,这个类是Runtime在程序运行的过程中动态创建的一个类,这个类继承自Person
。
2、在这个派生类里面调用了C函数:_NSSetIntValueAndNotify
。
3、在_NSSetIntValueAndNotify
里面实现如下代码:
[self willChangeValueForKey:@"age"]; [super setAge:age]; [self didChangeValueForKey:@"age"];
|
4、在didChangeValueForKey
方法里面去通知监听器某个属性值发生了改变。
用一张图来做一个总结:
未添加KVO监听的对象:

使用KVO添加监听的对象:

注意:
通过Runtime
中的object_class
拿到的class对象才是真正的class对象,通过class
拿到的不一定是真正的class对象,比如使用了KVO监听的对象。
如何验证派生类NSKVONotifing_Person
重写了哪些方法?
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
Class cls = object_getClass(self.person1); [self printMethodNameOfClass:cls];
- (void)printMethodNameOfClass:(Class)cls { unsigned int count; Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i < count; i++) { Method method = methodList[i]; NSLog(@"%@",NSStringFromSelector(method_getName(method))); } free(methodList); }
|
打印结果:
2022-11-16 13:36:32.484809+0800 OC对象的本质[26389:16561796] setAge: 2022-11-16 13:36:32.484965+0800 OC对象的本质[26389:16561796] class 2022-11-16 13:36:32.485084+0800 OC对象的本质[26389:16561796] dealloc 2022-11-16 13:36:32.485195+0800 OC对象的本质[26389:16561796] _isKVOA
|
从上面打印可以看到:NSKVONotifinh_Person
类重写了setAge:
、class
、dealloc
、_isKVOA
方法。
KVO的触发场景
从上面KVO的本质分析可以看到:只要有setter
方法就可以通过KVO来监听值的改变,比如:属性值发生改变、通过KVC赋值。
请看下面场景:
@interface Person : NSObject { @public int age; } @end
@implementation Person
@end
@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end @implementation KVOViewController
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[Person alloc] init]; self.person->age = 10;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:self forKeyPath:@"age" options:options context:nil]; }
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.person->age = 11; }
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"%@对象的%@属性发生了改变:\n%@",object, keyPath,change); }
- (void)dealloc { [self.person removeObserver:self forKeyPath:@"age"]; }
@end
|
如上:
通过self.person->age = 11
不会触发KVO,原因相信大家都很清楚了,没有调用setter
,可以在self.person->age = 11
前后分别添加[self.person willChangeValueForKey:@"age"]
和[self.person didChangeValueForKey:@"age"]
来手动触发KVO。
也可以通过KVC赋值[self.person setValue:@11 forKey:@"age"]
,这样就可以自动触发KVO。
其他补充
如何查看某个方法的函数地址(IMP)?
- (IMP)methodForSelector:(SEL)aSelector; + (IMP)instanceMethodForSelector:(SEL)aSelector;
|