iOS底层-KVO

150 阅读7分钟

KVO是一种机制,它允许对象在其他对象的指定属性发生更改时收到通知。它最常用的一个场景就是viewconroller中监听model属性的变化从而刷新页面展示。

KVO使用过程的细节

基本使用

  • [self.person addObserver:self forKeyPath:@"nickname" options:NSKeyValueObservingOptionNew context:NULL];
    
  • - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        if ([keyPath isEqualToString:@"nickname"]) {
            NSLog(@"%@",change);
        }
    }
    
  • [self.person removeObserver:self forKeyPath:@"nickname" context:NULL];
    

context参数的使用

  • [self.person addObserver:self forKeyPath:@"nickname" options:NSKeyValueObservingOptionNew context:NULL];
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        if ([keyPath isEqualToString:@"nickname"]) {
            NSLog(@"%@",change);
        }
    }
    
  • //context定义
    static void *PersonNickNameContext = &PersonNickNameContext;
    static void *PersonNameContext = &PersonNameContext;
    //注册观察者
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    //KVO的回调
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        if (context == PersonNickNameContext) {
            NSLog(@"%@",change);
        }else if (context == PersonNameContext){
            NSLog(@"%@",change);
        }
    }
    

是否有必要移除KVOobserver

官方文档对removeObserver也有说明:

When removing an observer, keep several points in mind:

  • Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.
  • An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
  • The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.

翻译过来就是,移除观察者时,注意以下几点:

  • 如果未注册为观察者,在移除观察者的时候会导致NSRangeException异常。removeObserver必须和addObserver对应,且只能调用一次。如果项目中不能保证,就需要在使用的时候使用try/catch来处理异常。
  • 观察者在对象销毁的时候不会自动移除观察者。被观察者会继续发送通知,对观察者来说这个状态是感知不到的。但是,向一个已经释放的对象发送通知会引起内存访问异常。所以,我们要保证观察者在内存释放之前移除观察。
  • 这个协议没有方法可以判断他是一个观察者还是被观察者,写代码是要避免释放内存相关的错误。一个典型的规范就是在观察者初始画的时候注册观察,在dealloc的时候移除观察,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来。 所以,总的来说,KVO注册观察者 和移除观察者是需要成对出现的,如果只注册,不移除,会出现野指针的崩溃

自动触发与手动触发

KVO观察的自动和手动两种方式

  • 自动开关,automaticallyNotifiesObserversForKey返回YES的时候标示自动监听,如果是NO表示我们需要手动监听

    // 自动开关
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
        return YES;
    }
    
  • 如果是手动,我们需要通过手动开关监听

    - (void)setNickName:(NSString *)nickName{
        //手动开关
        [self willChangeValueForKey:@"nickName"];
        _nickName = namenickName
        [self didChangeValueForKey:@"nickName"];
    }
    

观察多个属性变化

我们以观察两个属性为例,例如我们需要根据速度speed和时间time,取得当前的路程distance。我们用两种方式。

  • 第一种就是分别观察速度speed和时间time两个属性,当其中一个发生变化计算 当前路程distance
  • 第二种方式就是,通过keyPathsForValuesAffectingValueForKey方法,将两个观察合为一个观察,即观察当前路程distance
    //1、合二为一的观察方法
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"distance"]) {
            NSArray *affectingKeys = @[@"speed", @"time"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    ​
    //2、注册KVO观察
    [self.person addObserver:self forKeyPath:@"distance" options:(NSKeyValueObservingOptionNew) context:NULL];
    ​
    //3、触发属性值变化
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person.speed += 10;
        self.person.time  += 1;
    }
    ​
    //4、移除观察者
    - (void)dealloc{
        [self.person removeObserver:self forKeyPath:@"distance"];
    }
    ​

可变数组的观察

KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,所有对可变数组的KVO观察下面这种方式不生效的,即直接通过[self.person.dateArray addObject:@"1"];向数组添加元素,是不会触发kvo通知回调的,代码如下:

//1、注册可变数组KVO观察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    
//2、KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
​
//3、移除观察者
- (void)dealloc{
 [self.person removeObserver:self forKeyPath:@"dateArray"];
}
​
//4、触发数组添加数据
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person.dateArray addObject:@"1"];
}

The protocol defines three different proxy methods for collection object access, each with a key and a key path variant:

  • mutableArrayValueForKey: and mutableArrayValueForKeyPath: These return a proxy object that behaves like an NSMutableArray object.
  • mutableSetValueForKey: and mutableSetValueForKeyPath:These return a proxy object that behaves like an NSMutableSet object.
  • mutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:These return a proxy object that behaves like an NSMutableOrderedSet object. 我们代码这样修改:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}

现在可变数组就可以监听到了。

KVO底层探索

苹果官方文档在Key-Value Observing Implementation Details里有提到KVO的实现:

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的交换。当我们添加一个observer的时候isa的指向会发生改变,是一个中间类而不是真正的类。我们不能根据isa指针确定类的成员身份,而是用哪个class方法确定。

中间类是什么

看苹果官网文档我们了解了,KVO的实现时通过修改isa指针指向了一个中间类实现的,我们使用lldb探究一下中间类是什么。

  • 添加观察者之前,我们打印实例对象person的方法是JSPerson

    (lldb) po object_getClassName(self.person)
    "JSPerson"
    
  • 添加观察者之后,我们打印实例对象person的方法是NSKVONotifying_JSPerson

    (lldb) po object_getClassName(self.person)
    "NSKVONotifying_JSPerson"
    

通过上面的调试,我们看到添加观察者值isa指向了一个名为"NSKVONotifying_JSPerson的中间类。关于这个中间类我们有几个点需要研究一下。 中间类和之前的类是父子类关系吗

我们通过一个方法来判断

#pragma mark - 遍历类以及子类
- (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:[JSPerson class]];
​

打印结果为

classes = (
    JSPerson,
    "NSKVONotifying_JSPerson"
)

通过打印结果我们可以判断中间类NSKVONotifying_JSPersonJSPerson的子类。

  • 中间类里有什么方法。

    同样,我们定义一个方法获取NSKVONotifying_JSPerson的所有方法

    #pragma mark - 遍历方法-ivar-property
    - (void)printClassAllMethod:(Class)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:objc_getClass("NSKVONotifying_JSPerson")];
    

    打印结果

    setNickName:-0x7fff207bbb57
    class-0x7fff207ba662
    dealloc-0x7fff207ba40b
    _isKVOA-0x7fff207ba403
    

    我们看到一共有四个方法

    • 重写了父类的setNickName方法
    • 重写了根类的classdealloc方法
    • _isKVO方法,用来判断是否是kvo
  • dealloc中移除观察者后,isa会指回来吗

    • 移除观察者之前,我们用lldb打印

      (lldb) po object_getClassName(self.person)
      "NSKVONotifying_JSPerson"
      
    • 移除观察者之后,我们重新打印

      (lldb) po object_getClassName(self.person)
      "JSPerson"
      

    说明的确是在移除观察的时候将isa指回来的。

  • 移除观察后中间类会销毁吗

    我们返回前一个页面,此时添加观察者的VC已经销毁,我们打印JSPerson的子类

    [self printClasses:[JSPerson class]];
    

    打印结果

    classes = (
        JSPerson,
        "NSKVONotifying_JSPerson"
    )
    

    发现子类并不会被销毁。

小结

  • 实例对象isa的指向在添加KVO观察者之后,由原有类更改为指向中间类
  • 中间类重写了观察属性的setter方法classdealloc_isKVOA方法
  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类
  • 中间类从创建后,就一直存在内存中,不会被销毁