KVO探索

概述

  • KVO(Key-Value Observing) 是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,所以对属性才会发生作用,一般继承自 NSObject 的对象都默认支持 KVO。

  • KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO 是一对一的,而NSNotificationCenter是一对多的。KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

  • KVO 的实现依赖于 OC 强大的Runtime

  • KVO 是 Cocoa 提供的一种基于 KVC 的机制

实现原理

  1. KVO 是通过 isa-swizzling 技术实现的(这句话是整个 KVO 实现的重点)。

  2. 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(子类),在这个派生类中重写基类(父类)中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制。

  3. 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
    ,每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察监听,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法

    派生类

  4. 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就会记录旧的值。然后调用父类的setter方法更新key的值,而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

  5. KVO的这套实现机制中苹果还偷偷重写了-class()方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

使用

  • 注册观察者对象 :给对象Person的实例person(被观察者)通过 -addObserver:forKeyPath:options:context:添加观察者observer,指定要观察的属性keyPath,观察者就可以接受keyPath属性的变化事件。change含有kind、old、new三种
 typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew ,     //接受新值 默认
    NSKeyValueObservingOptionOld ,     //接受旧值
    NSKeyValueObservingOptionInitial , //在注册时立即接受一次回调,在改变时也会发送通知
    NSKeyValueObservingOptionPrior     //改变前发送一次,改变后发送一次
};

在调用addObserver后,KVO并不会对观察者进行强引用

  • 在观察者中实现 -observeValueForKeyPath:ofObject:change:context:方法,当keyPath变化时,KVO就会调用该方法通知观察者。object为被观察者对象。
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
kind指值得变化方式
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,    //通过观察设置方法
    NSKeyValueChangeInsertion = 2,  //通过观察插入方法(容器)
    NSKeyValueChangeRemoval = 3,    //remove
    NSKeyValueChangeReplacement = 4,//替换
};
context 传入任意类型的对象,在接受消息会调的代码中姐可以接受到该对象
  • 当观察者不需要监听时,使用-removeObserver: forKeyPath:移除KVO。要在观察者消失前调用,-dealloc中。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    /**
     根据 keyPath 确定KVO的出发模式是手动还是自动
     
     手动模式在key变化前后s需要手动调用 -willChangeValueForKey 和  -willChangeValueForKey
     [_person willChangeValueForKey:@"name"];
     _person.name = [NSString stringWithFormat:@"%d", a++];
     [_person willChangeValueForKey:@"name"];
     
     */
    if ([key isEqualToString:NSStringFromSelector(@selector(name))]) {
        return NO;
    }
    return YES;
}
/**
 属性依赖键 使对象获取其他对象的特定属性变化的通知机制
 */
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keypaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    /**
     当对当前类的dog属性添加观察者时,告诉系统dog的变化依赖于dog的那些属性的变化
     */
    if ([key isEqualToString:@"dog"]) {
        keypaths = [NSSet setWithObjects:@"_dog.color", @"_dog.age", nil];
    }
    return keypaths;
}

自定义

添加观察者
  1. 通过Method判断是否有这个key对应的setter selector(因为值的变化是重写setter方法来达到目的,所以setter必须有),如果没有则Crash。
  2. 判断当前类是否是KVO子类,如果不是 则创建,并设置其isa指针。(实例的isa指针指向该实例的类,类的isa指针指向它的元类)
  3. 如果没有实现,则添加Key对应的setter方法。
  4. 将调用对象添加到数组中。
    // 1.
    SEL setSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(object_getClass(self), setSelector);
    if (!setterMethod) {
        NSString *reason =  [NSString stringWithFormat:@"Object %@ dose not have a setter for key :  %@",self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    // 2.
    Class class = object_getClass(self);
    NSString *classname = NSStringFromClass(class);
    if (![classname hasPrefix:KXSKVOClassPrefix]) {
        class = [self makeKvoClassWithOriginalClassName:classname];
        object_setClass(self, class);
    }
    
    // 3.
    if (![self hasSelector:setSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(class, setSelector, (IMP)kvo_setter, types);
    }
    
    // 4.
    KXSObservationInfo *observerInfo = [[KXSObservationInfo alloc] initWithObserver:observer key:keyPath changeBlock:changeBlock];
    
    NSMutableArray *observerArr = objc_getAssociatedObject(self, KXSKVOObserverProperty);
    if (!observerArr) {
        observerArr = [NSMutableArray array];
    }
    [observerArr addObject:observerInfo];
    objc_setAssociatedObject(self, KXSKVOObserverProperty, observerArr, OBJC_ASSOCIATION_COPY);
创建子类,并设置其isa指针
  1. 判断是否存在KVO类,如果存在则返回。
  2. 如果不存在,则创建KVO类。
  3. 重写KVO类的class方法,指向自定义的IMP。
    // 1.
    NSString *kvoClazzName = [kISKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    // 2.
    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    
    /**
     创建一个类 为"class pair"分配空间

     @param superclass 继承的类
     @param name 类名
     objc_allocateClassPair(Class superclass, const char * name, 0)
     */
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    // 3.
    // grab class method's signature so we can borrow it 获取类中的某个实例方法(减号方法)
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    // types 返回值类型 -> @"v@;@"
    // [p add]  [p setName:name]
    // objc_msgSend(p, @selector(add))   objc_msgSend(p, @selector(setName), name)
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    // SEL selector 的简写,俗称方法选择器,实质存储的是方法的名称
    // IMP implement 的简写,俗称方法实现,看源码得知它就是一个函数指针
    // Method 对上述两者的一个包装结构.
    
    objc_registerClassPair(kvoClazz);
重写子类的setter方法
  1. 获取旧值。
  2. 创建super的结构体,并向super发送属性的消息。这样相当于是调用原来类的setter方法,这一步是必须的。
  3. 遍历调用回调。
// 1.
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    id oldValue = [self valueForKey:getterName];
    
    // 2.
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    // objc_msgSend(objc_super->receiver, _cmd)
    // 从父类的方法列表开始找 _cmd 方法
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // 3.
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self,kISKVOAssociatedObservers);
    for (ISObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
重写class方法调用
static Class kvo_class(id self, SEL _cmd)
{
    // 调用object_getClass函数后其返回的是一个Class类型,Class是objc_class定义的一个typedef别名,通过objc_class就可以获取到对象的isa指针指向的Class,也就是对象的类对象
    // 返回 类的父类 class_getSuperclass
    // 起到隐藏该子类,以“欺骗”外部调用者它就是起初的那个类
    return class_getSuperclass(object_getClass(self));
}

参考

facebook 的 KVOController
KVO底层原理及Block方式回调实现
KVC/KVO原理详解及编程指南
iOS开发-Runtime详解()
iOS开发-Runloop详解()
Type Encodings
super与objc_msgSendSuper
KVC/KVO原理详解及编程指南

你可能感兴趣的:(KVO探索)