kvo ,即key-value-observing(即键值观察),是苹果提供的一套事件通知机制。允许对象监听另一个对象属性的改变并且在属性值改变的时候接受到通知。一般继承自NSObject 的类都支持KVO。
使用KVO的前提:
这个类必须支持KVC(KVC 跟KVO的关系),支持跟KVC相同的数据类型。
(1)使用KVO
分为三个步骤:
1 :注册成为观察者。使用API:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2:实现回调方法:使用API:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
keypath 属性发生变化的时候回回调这个方法通知观察者。
3 当观察者不需要监听或者观察者销毁前,需要调用
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
这个方法需要在观察者销毁前调用,不然会crash。
(2)注册事件
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
这个注册方法参数options是一个枚举值,传入不同的值会有不同的观察效果:
NSKeyValueObservingOptionNew//接收新值,默认是接收新值
NSKeyValueObservingOptionOld//接收旧值
NSKeyValueObservingOptionInitial//在注册观察者之后立即接收一次通知
kvo 对观察者不会强引用。
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
回调方法中change参数是一字典,存放以NSKeyValueChangeKey为键KVO属性相关的值:
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;//只有当addObserver的时候在optional参数中加入NSKeyValueObservingOptionNew,这个键值对才会被change参数包含;它表示这个property改变后的新值。
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;//表示改变之后属性的新的值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;//表示改变之前的值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;//当被观察的property是一个ordered to-many relationship时,这个键值对才会被change参数包含;它的值是一个NSIndexSet对象。
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));//只有当addObserver的时候在optional参数中加入NSKeyValueObservingOptionPrior,这个键值对才会被change参数包含;它的值是@YES
(3)KVO使用场景
顾名思义,键值观察,就是观察属性变化,用来做键值观察操作。斯坦福大学的iOS公开课,在讲解MVC框架的时候,都提到model跟controller之间的通信是KVO实现的,是一个经典的使用场景。
KVO 的addObserver跟removeObserver是需要成对存在的,并且需要在正确的时机removeObserver不然会crash。一般在init的时候添加Observer,在dealloc的时候removeObserver。
(4)手动调用KVO
KVO是在被观察的类keypath对应属性被改变的时候会触发,如果我们想要手动控制这个调用时机,或者想自己实现kvo属性的调用,就可以用KVO提供的手动API去实现。
示例代码:
[self.person willChangeValueForKey:@"age"];
self.person.age = 10;
[self.person didChangeValueForKey:@"age"];
如果想要控制手动触发的过程,需要重写方法
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if([key isEqualToString:@"age"])
{
//这里需要调用super
automatic = [super automaticallyNotifiesObserversForKey:key];
}
else
{
automatic =NO;
}
return automatic;
}
返回YES则代表可以触发,返回NO,代表不可以触发。
如果去参加一个中型以上的互联网公司面试,关于KVO 的原理是必问的一环(iOS面试一般就是:KVO原理,多线程编程,运行时,运行循环,内存管理,block)。那么来探讨一下到底KVO是如何实现的。
KVO
是通过isa-swizzling
技术实现的(这句话是整个KVO
实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa
指向中间类。并且重写了派生类中被观察属性的set方法,手动调用KVO,即上文中willChangeValueForkey 跟didChangValueForkey;
通过代码测试一下KVO的实现过程:(此处代码参考博客:https://www.jianshu.com/p/badf5cac0130)
创建KVOObject 类,并且打印一些关键的数据
@interface KVOObject : NSObject
@property (nonatomic, copy ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation KVOObject
- (NSString *)description {
NSLog(@"object address : %p \n", self);
IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
Class objectMethodClass = [self class];
Class objectRuntimeClass = object_getClass(self);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
NSLog(@"object method list \n");
unsigned int count;
Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"method Name = %@\n", methodName);
}
return @"";
}
在另一个类中分别创建两个KVOObject
对象,其中一个对象被观察者通过KVO
的方式监听,另一个对象则始终没有被监听。在KVO
前后分别打印两个对象的关键信息,看KVO
前后有什么变化。
@property (nonatomic, strong) KVOObject *object1;
@property (nonatomic, strong) KVOObject *object2;
self.object1 = [[KVOObject alloc] init];
self.object2 = [[KVOObject alloc] init];
[self.object1 description];
[self.object2 description];
[self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 description];
[self.object2 description];
self.object1.name = @"lxz";
self.object1.age = 20;
控制台打印信息:
// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
我们发现对象被KVO
后,其真正类型变为了NSKVONotifying_KVOObject
类,已经不是之前的类了。KVO
会在运行时动态创建一个新类,将对象的isa
指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx
的格式。KVO
为了使其更像之前的类,还会将对象的class
实例方法重写,使其更像原类。
在上面的代码中还发现了_isKVOA
方法,这个方法可以当做使用了KVO
的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO
动态生成的类,就可以从方法列表中搜索这个方法。
那KVO是如何实现通知机制的呢?
重点:修改被观察属性的set方法
KVO会重写keyPath对应属性的setter方法,没有被KVO的属性则不会重写其setter方法。
在重写的setter方法中,修改值之前会调用willChangeValueForKey:方法,
修改值之后会调用didChangeValueForKey:方法,这两个方法最终都会被调用到
observeValueForKeyPath:ofObject:change:context:方法中。
系统KVO的缺点:
1: 不安全,不慎容易崩溃;
2:键值访问,字符串容易出错(反射机制可以解决)
3:复杂的属性关系会导致不好排查的bug。