KVO的常见问题
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
1.利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
2.当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
3.这个函数会去调用重写的setter方法。
setter方法内部实现---
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
- 如何手动触发KVO?
KVO一般都是自动就调用了。打点或者setValue调用。如果不这样操作也想调用kvo的监听,比如直接去修改成员变量(_属性)的值。就可以:
手动调用willChangeValueForKey:和didChangeValueForKey:
- (void)updateName:(NSString *)name {
[self willChangeVauleForKey:@"name"];
_name = name;
[self didChangeVauleForKey:@"name"];
}
KVO的自动调用也是可以控制的,重写如下方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"name"]) {
automatic = NO;//关闭了name属性的kvo监听的自动调用。手动如上操作还是可以调起kvo的监听。
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
- 直接修改成员变量的值会触发KVO么?
显而易见,不做特殊处理的话,不会触发kvo。
KVO的实现机制
那么,KVO背后是如何实现的呢?
主要说了两件事:
1.KVO是基于isa-swizzling
技术实现的。isa-swizzling
会将被观察对象的isa指针
进行替换。
2.因为在实现KVO时,系统会替换掉被观察对象的isa指针
,因此,不要使用isa指针
来判断类的关系,而应该使用class方法。
我们准备了下面的实验代码,来窥探一下KVO的实现机制,是不是如我们所说的那样。
@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSMutableArray *friends;
@end
@implementation Student
- (void)showObjectInfo {
NSLog(@"Object instance address is %p, Object isa content is %p", self, *((void **)(__bridge void *)self));
}
@end
我们在Student类中定义了方法- (void)showObjectInfo
,主要是用来打印Student实例的地址,以及Student 的isa指针
中的内容。这可以用来研究系统是如何做isa-swizzling
操作的。
然后准备下面的方法,来打印类的方法列表:
static NSArray * ClassMethodNames(Class c)
{
NSMutableArray * array = [NSMutableArray array];
unsigned int methodCount = 0;
Method * methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
运行如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
Student *std = [Student new];
// 1. 初始值
std.name = @"Tom";
NSLog(@"std->isa:%@", object_getClass(std));
NSLog(@"std class:%@", [std class]);
NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
[std showObjectInfo];
// 2. 添加KVO
[std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[std addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"std->isa:%@", object_getClass(std));
NSLog(@"std class:%@", [std class]);
NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
[std showObjectInfo];
std.name = @"Jack";
// 3. 移除KVO
[std removeObserver:self forKeyPath:@"name"];
[std removeObserver:self forKeyPath:@"friends"];
NSLog(@"std->isa:%@", object_getClass(std));
NSLog(@"std class:%@", [std class]);
NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
[std showObjectInfo];
}
运行可以看到输出
1.没添加kvo之前的初始值:
std->isa:Student
std class:Student
ClassMethodNames:(
showObjectInfo,
"setFriends:",
friends,
".cxx_destruct",
"setName:",
name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd
2.添加kvo之后的打印:
std->isa:NSKVONotifying_Student
std class:Student
ClassMethodNames:(
"setFriends:",
"setName:",
class,
dealloc,
"_isKVOA"
)
Object address is 0x28194fe80, Object isa content is 0x1a282b5bf05
3.移除kvo之后的打印
std->isa:Student
std class:Student
ClassMethodNames:(
showObjectInfo,
"setFriends:",
friends,
".cxx_destruct",
"setName:",
name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd
通过观察添加KVO前、添加KVO后,移除KVO后这三个实际的Object地址信息可以知道,Object的地址并没有改变,但是其isa指针
中的内容,却经历了如下变化:0x1a1008090cd->0x1a282b5bf05->0x1a1008090cd
。
对应的,通过object_getClass(std)
方法来输出std的类型是:Student->NSKVONotifying_Student->Student
这就是所谓的isa-swizzling
,当KVO时,系统会将被观察对象的isa指针内容做替换,让其指向新的类NSKVONotifying_Student
,而在移除KVO后,系统又会将isa指针内容还原。
那么,NSKVONotifying_Student
这个类又是什么样的呢?
通过打印其方法列表,可以知道,NSKVONotifing_Stdent
定义或重写了如下方法:
ClassMethodNames:(
"setFriends:",
"setName:",
class,
dealloc,
"_isKVOA"
)
可以看到,系统新生成的类重写了我们KVO的属性Friends
和Name
的set方法
。
同时,还重写了class方法
。通过runtime的源码可以知道,class方法:
- (Class)class {
return object_getClass(self);
}
+ (Class)class {
return self;
}
而在object_getClass
方法中,会输出实例的isa指向的类:
{
if (obj) return obj->getIsa();
else return Nil;
}
按说[std class]
和object_getClass(std)
的输出应该一致,但是系统会在KVO的时候,悄悄改写实例的class方法
。这也就是为什么,当使用[std class]
方法打印实例的类时,会输出Student
而不是实际的NSKVONotifing_Student
。
然后系统还重写了dealloc方法
,估计是为了在实例销毁时,做一些检查及清理工作。
最后,添加了_isKVOA
方法,这估计是系统为了识别是KVO类而添加的。
这里,细心的同学会发现,在KVO之前
,Student
的方法列表里面是包含属性的get方法
,showObjectInfo
方法以及.cxx_destruct
这些方法的。而当系统将Student
替换为NSKVONotifing_Student
后,这些方法那里去了呢?如果这些方法没有在NSKVONotifing_Student
再实现一遍的话,那当KVO后
,我们再调用属性的get方法
、showObjectInfo
方法岂不是会crash?
但平日的编程实践告诉我们,并不会crash
。那这些方法都去那里了呢?让我们来看一下NSKVONotifing_Student
的父类是什么:
// 2. 添加KVO
...
Class objectRuntimeClass = object_getClass(std);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"super class is %@", superClass);
输出为:super class is Student
哈哈,很有意思吧,原来NSKVONotifing_Student的父类竟然是Student。那根据OC的消息实现机制,当在NSKVONotifing_Student中没有找到方法实现时,会自动到其父类Student中寻找相应的实现。因此,在NSKVONotifing_Student中,仅仅需要定义或重写KVO相关的方法即可,至于Student中定义的其他方法,则会在消息机制中在被自动找到。
- 以上,便是KVO的isa-swizzling技术的大体实现流程。
1.当类实例被KVO后,系统会替换实例的isa指针内容。让其指向NSKVONotifing_XX
类型的新类。
2.在NSKVONotifing_XX
类中,会:重写KVO属性
的set方法
,支持KVO。重写class方法
,来伪装自己仍然是XX类。添加_isKVOA方法
,来说明自己是一个KVO类。重写dealloc方法
,让实例下析构时,好做一些检查和清理工作
3.为了让用户在KVO isa-swizzling
后,仍然能够调用原始XX类中的方法,系统还会将NSKVONotifing_XX类
设置为原始XX类的子类
。
4.当移除KVO后,系统会将isa指针
中的内容复原。
自己写一个KVO
既然知道了KVO背后的实现原理,我们能不能利用runtime方法,模拟实现一下KVO呢?
当然可以,下来看下效果:
#import "ViewController.h"
#import "NSObject+KVOBlock.h"
#import <objc/runtime.h>
@implementation Student
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *std = [Student new];
// 直接用block回调来接受 KVO
[std sw_addObserver:self forKeyPath:@"name" callback:^(id _Nonnull observedObject, NSString * _Nonnull observedKeyPath, id _Nonnull oldValue, id _Nonnull newValue) {
NSLog(@"old value is %@, new vaule is %@", oldValue, newValue);
}];
std.name = @"Hello";
std.name = @"Lilhy";
NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
[std sw_removeObserver:self forKeyPath:@"name"];
NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
}
@end
为了模拟的和系统KVO实现类似,我们也改写了class方法,在KVO移除前后,打印std的类信息为:
class is Student, object_class is sw_KVONotifing_Student
// 移除KVO后
class is Student, object_class is Student
在这里我手动实现了KVO,并通过Block的方式来接受KVO的回调信息。接下来我们就一步步的分析是如何做到的。我们应该重点观察所使用到的runtime方法。
首先,我们新建一个NSObject的分类NSObject (KVOBlock),并声明如下方法:
typedef void(^sw_KVOObserverBlock)(id observedObject, NSString *observedKeyPath, id oldValue, id newValue);
@interface NSObject (KVOBlock)
- (void)sw_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
callback:(sw_KVOObserverBlock)callback;
- (void)sw_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
@end
在关键的sw_addObserver:forKeyPath:callback:
中,是这么实现的:
static void *const sw_KVOObserverAssociatedKey = (void *)&sw_KVOObserverAssociatedKey;
static NSString *sw_KVOClassPrefix = @"sw_KVONotifing_";
- (void)sw_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
callback:(sw_KVOObserverBlock)callback {
// 1. 通过keyPath获取当前类对应的setter方法,如果获取不到,说明setter 方法即不存在与KVO类,也不存在与原始类,这总情况正常情况下是不会发生的,触发Exception
NSString *setterString = sw_setterByGetter(keyPath);
SEL setterSEL = NSSelectorFromString(setterString);
Method method = class_getInstanceMethod(object_getClass(self), setterSEL);
if (method) {
// 2. 查看当前实例对应的类是否是KVO类,如果不是,则生成对应的KVO类,并设置当前实例对应的class是KVO类
Class objectClass = object_getClass(self);
NSString *objectClassName = NSStringFromClass(objectClass);
if (![objectClassName hasPrefix:sw_KVOClassPrefix]) {
Class kvoClass = [self makeKvoClassWithOriginalClassName:objectClassName]; // 为原始类创建KVO类
object_setClass(self, kvoClass); // 将当前实例的类设置为KVO类
}
// 3. 在KVO类中查找是否重写过keyPath 对应的setter方法,如果没有,则添加setter方法到KVO类中
// 注意,此时object_getClass(self)获取到的class应该是KVO class
if (![self hasMethodWithMethodName:setterString]) {
class_addMethod(object_getClass(self), NSSelectorFromString(setterString), (IMP)sw_kvoSetter, method_getTypeEncoding(method));
}
// 4. 注册Observer
NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
if (observerArray == nil) {
observerArray = [NSMutableArray new];
objc_setAssociatedObject(self, sw_KVOObserverAssociatedKey, observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
SWKVOObserverItem *item = [SWKVOObserverItem new];
item.keyPath = keyPath;
item.observer = observer;
item.callback = callback;
[observerArray addObject:item];
}else {
NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
[exception raise];
}
}
上面的函数重点是:
1.调用makeKvoClassWithOriginalClassName
方法来生成原始类对应的KVO类
2.利用class_addMethod
方法,为KVO类
添加改写的setter实现
完成了上面两点,一个手工的KVO实现基本就完成了。另一个需要注意的是,如何存储observer。在这里是通过一个MutableArray数组,当做Associated object
来存储到类实例中的。
可以看出来,这里的重点在于如何创建原始类对应的KVO类
:
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClassName {
// 1. 检查KVO类是否已经存在, 如果存在,直接返回
NSString *kvoClassName = [NSString stringWithFormat:@"%@%@", sw_KVOClassPrefix, originalClassName];
Class kvoClass = objc_getClass(kvoClassName.UTF8String);
if (kvoClass) {
return kvoClass;
}
// 2. 创建KVO类,并将原始class设置为KVO类的super class
kvoClass = objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0);
objc_registerClassPair(kvoClass);
// 3. 重写KVO类的class方法,使其指向我们自定义的IMP,实现KVO class的‘伪装’
Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
const char* types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);
return kvoClass;
}
其实实现也不难,调用了runtime的方法
1.objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0)
动态生成新的KVO类
,并设置KVO类
的super class是原始类
2.注册KVO类 : objc_registerClassPair(kvoClass)
3.为了实现KVO伪装成原始类,还为KVO类
添加了我们自己重写的class方法
:
Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
const char* types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);
// 自定义的class方法实现
static Class sw_class(id self, SEL selector) {
return class_getSuperclass(object_getClass(self)); // 因为我们将原始类设置为了KVO类的super class,所以直接返回KVO类的super class即可得到原始类Class
}
那么当我们需要移除Observer时,需要调用sw_removeObserver:forKeyPath:
方法:
- (void)sw_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath {
NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
[observerArray enumerateObjectsUsingBlock:^(SWKVOObserverItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.observer == observer && [obj.keyPath isEqualToString:keyPath]) {
[observerArray removeObject:obj];
}
}];
if (observerArray.count == 0) { // 如果已经没有了observer,则把isa复原,销毁临时的KVO类
Class originalClass = [self class];
Class kvoClass = object_getClass(self);
object_setClass(self, originalClass);
objc_disposeClassPair(kvoClass);
}
}
注意,这里当Observer数组为空时,我们会将当前实例的所属类复原成原始类,并dispose掉生成的KVO类。
原码