KVO简介
KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承至NSObject的对象都默认支持KVO。
KVO和NSNotificatioCenter都是ios中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,NSNotificatioCenter是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
KVO的基本使用
使用KVO分为三个步骤:
- 通过
addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。 - 在观察者中实现
observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。 - 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
注册观察者
在注册观察者时,可以传入options参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。
/** 注册观察者
@param observer 观察者
@param keyPath 要观察的属性keyPath
@param options 观察者选项。影响通知的生成方式及回调时字典中携带的信息
@context
*/
/// @param context 上下文
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
回调方法
观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。change字典中存放KVO属性相关的值,根据options时传入的枚举来返回。枚举会对应相应key来从字典中取出值,例如有NSKeyValueChangeOldKey字段,存储改变之前的旧值。
change中还有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平级的关系,来提供本次更改的信息,对应NSKeyValueChange枚举类型的value。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting。
如果观察者observer和他的父类是由于不同的原因都注册了对person.name属性的观察,或者多个对但是keypath相同,在回调中这两种的处理是不同的,那么回调中的keyPath和被观察者对象是无法区分的,此时就可以通过context这个参数来区分,更加便利 ,更加安全。它接收一个void *类型的参数,基本可以传任何类型。
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext;
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@property (nonatomic, strong) LGStudent *student;
@end
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.student = [LGStudent shareInstance];
// OC -> c 超集
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew)|(NSKeyValueObservingOptionOld) context:&PersonNickContext];
[self addObserver:self forKeyPath:@"self.person.name" options:(NSKeyValueObservingOptionNew)|(NSKeyValueObservingOptionOld) context:&PersonNameContext];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:&StudentNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@"self.person");
}
else if (context == StudentNameContext){
NSLog(@"self.student");
}
else {
NSLog(@"self");
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"YX";
self.student.name = @"森海北语";
}
输出信息:
2020-02-17 14:53:22.711221+0800 001---KVO初探[20704:225264] self
2020-02-17 14:53:22.711637+0800 001---KVO初探[20704:225264] self.person
2020-02-17 14:53:22.711912+0800 001---KVO初探[20704:225264] self.student
如果被观察对象是集合对象,在NSKeyValueChangeKindKey字段中会包含NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement的信息,表示集合对象的操作方式。
我们点进去NSKeyValueChange可以看到以上所说的
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
举例:
// ✅ 在LGPerson类里面添加一个数组属性
@property (nonatomic, strong) NSMutableArray *dateArray;
**********************
// ✅ 注册数组监听
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
**********************
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 数组变化
[self.person.dateArray addObject:@"1"];
// KVO 建立在 KVC
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
[[self.person mutableArrayValueForKey:@"dateArray"] removeObject:@"2"];
[[self.person mutableArrayValueForKey:@"dateArray"] replaceObjectAtIndex:0 withObject:@"3"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@ ",change);
}
如果在touches方法中只写[self.person.dateArray addObject:@"1"];,运行什么也输出不了,因为它触发不了KVO的回调,要进行[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];才能触发KVO回调,看输出:
2020-02-17 15:06:12.582853+0800 001---KVO初探[21559:237325] LGViewController - {
indexes = "<_NSCachedIndexSet: 0x600003c06f80>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
2
);
}
2020-02-17 15:06:12.583487+0800 001---KVO初探[21559:237325] LGViewController - {
indexes = "<_NSCachedIndexSet: 0x600003c06f80>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 3;
}
2020-02-17 15:06:12.584670+0800 001---KVO初探[21559:237325] LGViewController - {
indexes = "<_NSCachedIndexSet: 0x600003c06f60>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
3
);
}
KVO兼容的调用模式:
// 直接调用set方法,或者通过属性的点语法间接调用
[account setName:@"Savings"];
// 使用KVC的setValue:forKey:方法
[account setValue:@"Savings" forKey:@"name"];
// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"Savings" forKeyPath:@"account.name"];
// 通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
多个相关属性观察
例如如有一个LGPerson类,有三个属性totalData,writtenData,和百分比进度downloadProgress:
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
界面显示我们只关注downloadProgress,但进度是受其他两个属性共同影响的,此时需要在LGPerson实现中重写两个方法:
// 下载进度 -- writtenData/totalData
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress{
if (self.writtenData == 0 || self.totalData == 0) {
return @"0";
}
double progress = (double)self.writtenData / (double)self.totalData * 100;
if (progress > 100) {
progress = 100;
}
return [NSString stringWithFormat:@"%d%%", (int)ceil(progress)];
}
我们只需要观察downloadProgress这一个属性就可以了:
self.person.totalData = 100;
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
**********************
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 展开 - 折叠 -
self.person.writtenData += 10;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@",change);
}
输出:
2020-02-17 16:35:48.869752+0800 001---KVO初探[27031:301893] LGViewController - {
kind = 1;
new = "10%";
}
2020-02-17 16:35:50.259624+0800 001---KVO初探[27031:301893] LGViewController - {
kind = 1;
new = "20%";
}
移除观察者
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash
[self.person removeObserver:self forKeyPath:@"name"];
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
手动调用KVO
KVO在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
可以看到调用KVO主要依靠两个方法,在属性发生改变之前调用willChangeValueForKey:方法,在发生改变之后调用didChangeValueForKey:方法。
如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
KVO底层实现原理
KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
为了测试KVO的实现方式,我们加入下面的测试代码。首先创建一个KVOObject类,并在里面加入两个属性,然后重写description方法,并在内部打印一些关键参数。
@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;
下面是KVO前后打印的关键信息,我们在下面做详细分析。
// 第一次
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会重写keyPath对应属性的setter方法,没有被KVO的属性则不会重写其setter方法。在重写的setter方法中,修改值之前会调用willChangeValueForKey:方法,修改值之后会调用didChangeValueForKey:方法,这两个方法最终都会被调用到observeValueForKeyPath:ofObject:change:context:方法中。
到这里我们会有一个疑问,为什么上面调用runtime的object_getClass函数,就可以获取到真正的类呢?
因为NSKVONotifying_KVOObject重写了class方法,在这个方法中返回为KVOObject。但是object_getClass获取到的是isa指针,所以调用object_getClass返回的是NSKVONotifying_KVOObject。
总结
苹果提供的KVO自身存在很多问题,首要问题在于,KVO如果使用不当很容易崩溃。例如;
重复add和remove导致的Crash。
Observer被释放导致的崩溃。
keyPath传错导致的崩溃等。
在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。
KVO是一种事件绑定机制的实现,在keyPath对应的值发生改变后会回调对应的方法。这种数据绑定机制,在对象关系很复杂的情况下,很容易导致不好排查的bug。例如keyPath对应的属性被调用的关系很复杂,就不太建议对这个属性进行KVO。