KVO相关知识以及底层实现
一.KVO的基本使用
使用KVO分为三个步骤:
1.通过addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context方法添加观察者,观察者可以接受keyPath属性的变化事件。
2.在观察者中实现-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context方法,当keyPath属性发生变化后,KVO回调这个方法通知观察者。
3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则导致Crash
代码实现:
新建Student类,定义属性
@interface Student : NSObject
@property (strong, nonatomic) NSString * name;
@property (strong, nonatomic) NSMutableArray * things;
@property (strong, nonatomic) Dog * dog;
@end
控制器实现观察Student类的属性
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_student = [[Student alloc] init];
[self.student addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.student.name = [NSString stringWithFormat:@"%ld",random()];
}
控制器dealloc中移除监听
- (void)dealloc {
[self.student removeObserver:self forKeyPath:@"name"];
}
二.KVO出发模式
KVO在属性发生改变时调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO属性的调用,可以通过KVO提供的方法进行调用.
在Student中修改为手动模式:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
这里可以通过KEY值来修改,指定手动触发的属性:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
实现手动触发:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.student willChangeValueForKey:@"name"];
self.student.name = [NSString stringWithFormat:@"%ld",random()];
[self.student didChangeValueForKey:@"name"];
}
这里有一个问题:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.student willChangeValueForKey:@"name"];
// self.student.name = [NSString stringWithFormat:@"%ld",random()];
[self.student didChangeValueForKey:@"name"];
}
这个代码还是会触发,这里和底层原理有关,稍后分析。
三.KVO属性依赖
新建一个Dog类:
@interface Dog : NSObject
@property (assign, nonatomic) int age;
@property (strong, nonatomic) NSString * name;
@end
@interface Student : NSObject
@property (strong, nonatomic) NSString * name;
@property (strong, nonatomic) NSMutableArray * things;
@property (strong, nonatomic) Dog * dog;
@end
@implementation Student
- (instancetype)init {
if (self = [super init]) {
_dog = [[Dog alloc] init];
_things = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
@end
通过路径观察:
[self.student addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.student.dog.age = random()%100;
}
实际开发中有这样的需求:监听Student的Dog的变化,不管Dog发生什么样的变化,都要通知Student.
这个就是一种依赖,有一些属性的值取决于一个或者多个其他对象的属性值,一旦某个依赖的属性值变了,依赖它的属性的变化也需要被通知。
[self.student addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
在Student中实现:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet * set = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
set = [NSSet setWithObjects:@"_dog.age",@"_dog.name", nil];
}
return set;
}
四.KVO原理探究
大家应该都听说过KVO内部是监听set方法的吧,那么我们就来看看,它是怎么一回事!
我们将属性改成成员变量:
@interface Student : NSObject
{
@public
NSString * name;
}
为该成员变量添加监听:
[self.student addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionNew context:nil];
修改值:
self.student->name = [NSString stringWithFormat:@"%ld",random()];
测试结果:监听不到
结论:KVO是通过监听一个对象有没有调用set方法,然后set方法。
KVO底层实现:
1.自定义Sudent类的子类
2.重写setName,在内部恢复父类做法,通知观察者,
3.让外界调用Student类的子类方法,修改当前对象isa指针,指向自定义子类

苹果修改了isa指针。
五.简单模拟KVO
先来看看苹果的做法:

创建分类:
@interface NSObject (JKVO)
- (void)JENSEN_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
@implementation NSObject (JKVO)
- (void)JENSEN_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
const char * jClassName = [[@"JENSENKVO_" stringByAppendingString:NSStringFromClass([self class])]UTF8String];
Class jensenClass = objc_allocateClassPair([self class], jClassName, 0);
class_addMethod(jensenClass, @selector(setName:), (IMP)setName, "v@:@");
objc_registerClassPair(jensenClass);
object_setClass(self, jensenClass);
objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void setName(id self,SEL _cmd, NSString * newName){
Class class = [self class];
object_setClass(self ,class_getSuperclass(class));
objc_msgSend(self,@selector(setName:),newName);
id observer = objc_getAssociatedObject(self, @"objc");
if (observer) {
objc_msgSend(observer,@selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new":[self valueForKey:@"name"],@"kind":@1},nil);
}
object_setClass(self, class);
}
@end
六.KVO对容器类的监听
Student类:
@interface Student : NSObject
@property (strong, nonatomic) NSString * name;
@property (strong, nonatomic) NSMutableArray * things;
@property (strong, nonatomic) Dog * dog;
@end
@implementation Student
- (instancetype)init {
if (self = [super init]) {
_dog = [[Dog alloc] init];
_things = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
@end
控制器实现监听:
_student = [[Student alloc] init];
[self.student addObserver:self forKeyPath:@"things" options:NSKeyValueObservingOptionNew context:nil];
在容器中添加数据:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.student.things addObject:@"00"];
}
结果:监听不到容器变化
为什么?KVO监听的是set方法!addObject方法和things的set方法没有关系吧
所以我们改一下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray * tempArray = NSMutableArray.array;
[tempArray addObject:@"00"];
self.student.things = tempArray;
}
这样就能够监听到了吧!
但是如果我们需要观察这个容器类属性内部的变化呢?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray * tempArray = [self.student mutableArrayValueForKey:@"things"];
[tempArray addObject:@"00"];
}
运行结果:

kind为2,我们看一下kind定义的头文件:

这个时候,我们就观察到容器类插入方法了。
那么这种是怎么做到的呢?

这个NSKeyValueNotifyingMutableArray就是NSMutableArray的子类!
内部也是一个道理,重写了addObject方法,多了一个手动通知KVO的willChange和DidChange的吧。