iOS 一探KVO原理究竟

923 阅读10分钟

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 方法里,进行一系列的处理之后,再通过回调告知外界。