KVO概念
KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而NSNotificationCenter是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
KVO底层原理探索
原理探索都是基于这段代码
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
@implementation Person
@end
// NHViewController.m
#import "NHViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface NHViewController ()
@property (nonatomic, strong) Person* person;
@end
@implementation NHViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person = [[Person alloc] init];
NSLog(@"---- object_getClass(self.person) : %@ ----",NSStringFromClass(object_getClass(self.person)));
[self printfClassMethod:object_getClass(self.person)];
NSLog(@"[self.person class] : %@",NSStringFromClass([self.person class]));
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"---- object_getClass(self.person) : %@ ----",NSStringFromClass(object_getClass(self.person)));
[self printfClassMethod:object_getClass(self.person)];
NSLog(@"[self.person class] : %@",NSStringFromClass([self.person class]));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"Noah";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"object:%@ -- change:%@",object,change);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}
#pragma mark 打印类的全部方法
- (void)printfClassMethod:(Class)class{
unsigned int count = 0;
Method *methodList = class_copyMethodList(class, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = method_getImplementation(method);
NSLog(@"SEL:%@ -- IMP:%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
@end
- 首先我们打个断点在
self.person = [[Person alloc] init];这一行
- 进入断点之后用lldb调试,打印出当前对象isa指针指向的类,添加观察者之前先打印一次

- 添加观察者之后再次打印一次
可以看出,添加观察者之后,实例对象的isa指向发生了改变,接下来我们来看看类里面的方法,把注释打开
打印如下:
2020-01-22 18:48:39.121268+0800 KVO_Demo[3593:148194] ---- object_getClass(self.person) : Person ----
2020-01-22 18:48:39.121386+0800 KVO_Demo[3593:148194] SEL:.cxx_destruct -- IMP:0x106e37e20
2020-01-22 18:48:39.121450+0800 KVO_Demo[3593:148194] SEL:name -- IMP:0x106e37db0
2020-01-22 18:48:39.121492+0800 KVO_Demo[3593:148194] SEL:setName: -- IMP:0x106e37de0
2020-01-22 18:48:39.121770+0800 KVO_Demo[3593:148194] ---- object_getClass(self.person) : NSKVONotifying_Person ----
2020-01-22 18:48:39.121818+0800 KVO_Demo[3593:148194] SEL:setName: -- IMP:0x107192d1a
2020-01-22 18:48:39.121875+0800 KVO_Demo[3593:148194] SEL:class -- IMP:0x10719174e
2020-01-22 18:48:39.121917+0800 KVO_Demo[3593:148194] SEL:dealloc -- IMP:0x1071914f2
2020-01-22 18:48:39.121963+0800 KVO_Demo[3593:148194] SEL:_isKVOA -- IMP:0x1071914ea
可以看到,NSKVONotifying_Person这个类重写了setName,class,dealloc方法
- 接下来我们看看class方法的返回值,把注释打开
打印:
2020-01-22 19:14:27.928370+0800 KVO_Demo[3830:156153] ---- object_getClass(self.person) : Person ----
2020-01-22 19:14:27.928448+0800 KVO_Demo[3830:156153] SEL:.cxx_destruct -- IMP:0x108bbee20
2020-01-22 19:14:27.928496+0800 KVO_Demo[3830:156153] SEL:name -- IMP:0x108bbedb0
2020-01-22 19:14:27.928560+0800 KVO_Demo[3830:156153] SEL:setName: -- IMP:0x108bbede0
2020-01-22 19:14:27.928622+0800 KVO_Demo[3830:156153] [self.person class] : Person
2020-01-22 19:14:27.928841+0800 KVO_Demo[3830:156153] ---- object_getClass(self.person) : NSKVONotifying_Person ----
2020-01-22 19:14:27.928902+0800 KVO_Demo[3830:156153] SEL:setName: -- IMP:0x108f19d1a
2020-01-22 19:14:27.928957+0800 KVO_Demo[3830:156153] SEL:class -- IMP:0x108f1874e
2020-01-22 19:14:27.929008+0800 KVO_Demo[3830:156153] SEL:dealloc -- IMP:0x108f184f2
2020-01-22 19:14:27.929065+0800 KVO_Demo[3830:156153] SEL:_isKVOA -- IMP:0x108f184ea
2020-01-22 19:14:27.929116+0800 KVO_Demo[3830:156153] [self.person class] : Person
从打印可以看出两次的打印都是Person
结论:
KVO 底层实现:首先KVO需要创建一个子类(NSKVONotyfing_Person),这个子类是继承于被观察对象的,这个子类需要重写属性的setter方法,这个时候,外界在调用setter方法的时候,调用的是子类重写的setter方法。就是让外界的person对象的isa指针指向这个子类。
简单自定义KVO
// NSObject+NHKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (NHKVO)
- (void)nh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)nh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
NS_ASSUME_NONNULL_END
// NSObject+NHKVO.m
#import "NSObject+NHKVO.h"
#import <objc/message.h>
static NSString *const kNHKVOPrefix = @"NHKVONotifying_";
static NSString *const kNHKVOAssiociateKey = @"kNHKVO_AssiociateKey";
@implementation NSObject (KVO)
- (void)nh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 验证是否存在setter方法
[self judgeSetterMethodFromKeyPath:keyPath];
// 动态创建子类
Class class = [self createChildClassWithKeyPath:keyPath];
// 重定向isa,指向新建的动态子类
object_setClass(self, class);
// 保存观察者,当观察的属性发生改变时,向这个对象发送消息
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kNHKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)nh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
// class IMP
Class nh_class(SEL id,IMP _cmd){
return nil;
}
static void nh_setter(id self,SEL _cmd,id newValue){
// 消息转发 : 转发给父类
// 改变父类的值 --- 可以强制类型转换
void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// void /* struct objc_super *super, SEL op, ... */
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
//objc_msgSendSuper(&superStruct,_cmd,newValue)
lg_msgSendSuper(&superStruct,_cmd,newValue);
// 既然观察到了,下一步不就是回调 -- 让我们的观察者调用
// - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
// 1: 拿到观察者
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kNHKVOAssiociateKey));
// 2: 消息发送给观察者
SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
}
#pragma mark - 动态创建子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kNHKVOPrefix,NSStringFromClass([self class])];
Class newClass = NSClassFromString(newClassName);
if (newClass) return newClass;
// 如果内存中没有,则动态生成
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
// 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
// 从父类中获取class方法
Method classMethod = class_getInstanceMethod([self class], classSEL);
// 获取方法类型
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)nh_class, classTypes);
// 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)nh_setter, setterTypes);
return newClass;
}
#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
Class superClass = object_getClass(self);
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method method = class_getInstanceMethod(superClass, setterSEL);
if (!method) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有找到%@的setter方法",keyPath] userInfo:nil];
}
}
#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;
}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end
KVO使用
自动观察和手动观察
//如果需要自定义,需要重新此方法,默认返回YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
//在set方法中手动调用,变化类型只是针对NSKeyValueChangeSetting
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
属性依赖
第一种、
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key NS_AVAILABLE(10_5, 2_0);
说明:
1、返回目标属性依赖属性的KeyPath的Set。当对象注册后,KVO自动监测该对象所有的KeyPaths。
2、其默认实现从对象所属类的方法列表中匹配方法:+keyPathsForValuesAffecting<Key>(<Key>为属性名,比如Name),如果存在执行并返回结果;如果不存在向底层寻找已经废弃的方法+setKeys:triggerChangeNotificationsForDependentKey:
3、可以用来替换手动调用-willChangeValueForKey:/-didChangeValueForKey:来实现属性依赖的解决方案
4、不能在已有类的Category中使用,在Category禁止重写此方法,可以使用+keyPathsForValuesAffecting<Key>来实现。
第二种、
或者重写此格式+keyPathsForValuesAffecting<Key>(<Key>为属性名,比如Name)的方法名
观察数据类型
// 这种添加值的方法监听不到
[self.person.dateArray addObject:@"hello"];
// 需要使用KVC获取值出来才能监听到变化
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"hello"];