1、前言
KVO:(Key-Value-Observer) 是 iOS 中一种机制,这种机制允许将其他对象的特定属性的更改通知给对象,为 iOS 开发者们提供了很多的便利,我们可以使用 KVO 来检测对象属性的变化、快速做出响应,这能够为我们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。下面将介绍 KVO 的简单使用以及探究底层是如何实现的。
2、KVO 简单使用
1.注册观察者
Person *p = [[Person alloc] init];
/**
observer:观察者,也就是KVO通知的订阅者
keyPath:描述将要观察的属性,相对于被观察者
options:KVO的一些属性配置,有四个选项
context: 上下文,这个会传递到订阅着的函数中,用来区分消息,区分不同对象KeyPath相同时的情况
*/
[p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
/**
options 的四个枚举值
NSKeyValueObservingOptionNew: change字典包括改变后的值
NSKeyValueObservingOptionOld: change字典包括改变前的值
NSKeyValueObservingOptionInitial: 注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior: 值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
*/
2.实现回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
// 观察到属性值变化的时候,可以自定义操作
NSLog(@"%@",change);
}
}
3.移除观察者
- (void)dealloc {
// 在销毁的时候必须要移除观察者,不然可能会发生不可预期的bug
[p removeObserver:self forKeyPath:@"name"];
}
- 注意点
在观察对象可变数组的属性时,当可变数组通过
addObject:添加对象的时候,是无法观察到的,因为addObject:方法是不走setter方法的,而需要通过mutableArrayValueForKey:获取到可变数组,再添加对象即可。
4.手动触发 KVO
可能有时候,我们根据项目需求要实现手动触发 KVO,或者我们实现的类库不希望被 KVO。 这时候需要关闭自动生成 KVO 通知,然后手动的调用,手动通知的好处就是,可以灵活加上自己想要的判断条件。
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@end
@implementation Person
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name= name;
[self didChangeValueForKey:@"name"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
首先实现类方法 automaticallyNotifiesObserversForKey,并设置对需要手动触发的某一个属性的 key 不自动发送通知(返回 NO 即可)。这里要注意如果需要禁用整个类的 KVO,直接返回 NO 即可。其次手动实现属性的 setter 方法,并在赋值操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey 方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了。
3、KVO 原理探索
苹果官方文档的说明如下:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called `isa-swizzling`.
The `isa` pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the `isa` pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the `isa` pointer does not necessarily reflect the actual class of the instance.
You should never rely on the `isa` pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
根据官方文档得知: KVO 是通过 isa-swizzling 技术实现的。
KVO基本的流程就是编译器自动为被观察对象创造一个派生类,并将被观察对象的isa指向这个派生类。如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个属性 的setter方法,并在其中添加进行通知的代码。所以当被观察对象的属性发生变化的时候,又因为被观察对象的isa指向了派生类,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了重写,并添加了通知代码,因此会向注册的对象发送通知。
1. 派生类的探索
首先自定义一个打印类以及子类的方法。
// 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
然后再在添加观察者代码的前后打印这个类以及子类。
[self printClasses:[Person class]];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[Person class]];
查看打印结果,得知当在对象被观察者后,会生成 NSKVONotifying_xxx 的派生类。
2020-02-21 14:39:40.112287+0800 KVO[8230:2535168] classes = (
Person
)
2020-02-21 14:39:40.115580+0800 KVO[8230:2535168] classes = (
Person,
"NSKVONotifying_Person"
)
2. 重写 setter 的探索
自定义一个遍历类中所有方法的方法。
// 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********%@***********",NSStringFromClass(cls));
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
然后再在添加观察者代码的前后打印这个类以及其子类中的方法。
[self printClassAllMethod:[Person class]];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
查看打印结果,得知动态子类重写了被观察对象属性的 setter 方法。
2020-02-21 14:50:39.038882+0800 KVO[8317:2621892] *********Person***********
2020-02-21 14:50:39.039017+0800 KVO[8317:2621892] .cxx_destruct-0x1085c9450
2020-02-21 14:50:39.039139+0800 KVO[8317:2621892] name-0x1085c93e0
2020-02-21 14:50:39.039257+0800 KVO[8317:2621892] setName:-0x1085c9410
2020-02-21 14:50:39.039675+0800 KVO[8317:2621892] *********NSKVONotifying_Person***********
2020-02-21 14:50:39.039800+0800 KVO[8317:2621892] setName:-0x7fff25721c7a
2020-02-21 14:50:39.039920+0800 KVO[8317:2621892] class-0x7fff2572073d
2020-02-21 14:50:39.040048+0800 KVO[8317:2621892] dealloc-0x7fff257204a2
2020-02-21 14:50:39.040181+0800 KVO[8317:2621892] _isKVOA-0x7fff2572049a
3. 小拓展
- 重写
class方法,是为了伪装类,当修改了isa指向后,class的返回值不会变。 - 重写
dealloc方法,是为了释放资源。 - 重写
_isKVOA方法,是用来标示该类是一个KVO机制声称的类。
4、自定义函数式KVO
当我们在使用 KVO 的时候,既需要进行注册成为某个对象属性的观察者,还要在合适的时间点将自己移除,再加上需要覆写一个又臭又长的方法,并在方法里判断这次是不是自己要观测的属性发生了变化,所以一用 KVO 就感觉很头疼,那有没有一种更优雅的解决方案?
分析完 KVO 原理之后,我们可以通过函数式编程,一行代码监测我们需要观察对象的属性。
1. 准备工作
创建 NSObject 分类,因为我们需要任何对象都可以调用这个方法进行观察。
NSObject+KVO.h
// 当属性发生改变的回调 block
typedef void(^KVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface NSObject (KVO)
- (void)custom_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block;
@end
NSObject+KVO.m
#import <objc/message.h>
// 动态生成中间类的前缀
static NSString *const cKVOPrefix = @"cKVONotifying_";
// 关联对象 key
static NSString *const cKVOAssiociateKey = @"cKVO_AssiociateKey";
// 保存外界传入进来的信息,这个可以根据自己的需求自定义
@interface KVOInfo : NSObject
// 观察对象
@property (nonatomic, weak) NSObject *observer;
// 观察属性
@property (nonatomic, copy) NSString *keyPath;
// 回调 block
@property (nonatomic, copy) CustomKVOBlock handleBlock;
@end
@implementation KVOInfo
// 初始化,保存外界传进来的属性
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(CustomKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
- (void)custom_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(CustomKVOBlock)block{
// 1: 验证是否存在setter方法
[self judgeSetterMethodFromKeyPath:keyPath];
// 2: 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3: isa的指向 : LGKVONotifying_LGPerson
object_setClass(self, newClass);
// 4: 保存信息
KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(cKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(cKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}
#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
2. 检测属性是否存在 setter 方法
如果对象是成员变量,不会自动生成 setter 方法,所以也没有观察的必要。
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有当前%@的setter",keyPath] userInfo:nil];
}
}
3. 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 拼接子类的类名称
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",cKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重复创建生成新类
if (newClass) return newClass;
// 1: 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2: 注册类
objc_registerClassPair(newClass);
// 3: 添加class,伪装类,class的指向是外界观察的类
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)custom_class, classTypes);
// 4: 添加setter,赋值以及通知外界
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)custom_setter, setterTypes);
// 5: 添加dealloc,进行 isa 的重新指向
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)custom_dealloc, deallocTypes);
return newClass;
}
3.1 添加 class
Class custom_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
3.2 添加 setter
static void custom_setter(id self,SEL _cmd,id newValue){
NSLog(@"来了:%@",newValue);
// 从setter方法获取getter方法的名称
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
// 通过 KVC 获取旧值
id oldValue = [self valueForKey:keyPath];
// 消息转发 : 转发给父类
// 自定义 objc_msgSendSuper
void (*custom_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
// 给父类属性发送 setter 方法,保存新值
custom_msgSendSuper(&superStruct,_cmd,newValue);
// 信息数据回调,通过关联对象取出数组
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(cKVOAssiociateKey));
// 遍历数组取出 info,通过 info 的 block 回调给外界
for (KVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
3.3 添加 dealloc
static void custom_dealloc(id self,SEL _cmd){
// 销毁的时候,将 isa 指回父类
Class superClass = [self class];
object_setClass(self, superClass);
}
至此简单版的自定义函数 KVO 就完成了。但是还存在很多的问题,比如线程安全问题,销毁时内存的处理等等,这里我只是根据自己探索写出来的一些思路,仅供参考。如果想深入研究,可以去看看由 FaceBook 开源的 KVOController。
4、总结
通过上面的探索,得知 KVO 的底层原理是:当你在注册观察者的时候,系统会动态的创建 NSKVONotifying_xxx 的派生类,然后被观察对象的 isa 指向该子类,子类重写了观察属性的 setter 方法,当属性发生变化的时候,会发送消息到子类的 setter 方法里,进行一系列的处理之后,再通过回调告知外界。