KVO 的进一步理解

这是一篇简单又丰富的,周末愉快!

一、KVO概要及简单使用

KVO 就是一种监听,那是如何做到监听的呢?首先创建一个简单的 Class,代码如下:

#import 

@interface KVOObject : NSObject

// 姓名
@property (nonatomic, copy) NSString* name;

@end



#import "KVOObject.h"

@implementation KVOObject

@end

很简单, 就一个 Class,然后定义了一个 name 属性而已。

一个简单的试验如下:

// 创建一个 KVO 对象
KVOObject* kvObj = [[KVOObject alloc] init];
kvObj.name = @"HG";

// 添加 KVO 监听
[kvObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

kvObj.name = @"CoderHG";

// 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
[kvObj removeObserver:self forKeyPath:@"name"];

具体的监听方法代码如下:

// KVO 的系统监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"%@, %@, %@", keyPath, object, change);
}

以上就是一个简单而完整的 KVO 的使用场景,但是具体是什么原理呢?

二、KVO 的原理初步认识

都知道一个对象一旦添加了 KVO 监听,在本质上是系统动态的改变了该对象的 isa 指针。如果对 isa 不了解的话,可以看这个 OC 小专题。
想要知道 isa 有什么样的变动,先实现如下一个方法:

// 打印具体的 cls 中的方法信息
- (NSString*)printMethodNamesOfClass:(Class)cls {
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 返回类名与方法列表
    return [NSString stringWithFormat:@"类名: %@ \n方法列表: %@", NSStringFromClass(cls), methodNames];
}

然后将以上的试验修改一下,如下:

// 常规用法
- (void)convention {
    // 创建一个 KVO 对象
    KVOObject* kvObj = [[KVOObject alloc] init];
    kvObj.name = @"HG";
    
    Class cls = object_getClass(kvObj);
    NSString* isaInfo = [self printMethodNamesOfClass:cls];
    
    NSLog(@"\n\n添加 KVO 之前:\n%@", isaInfo);
    
    
    // 添加 KVO 监听
    [kvObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
    
    kvObj.name = @"CoderHG";
    
    cls = object_getClass(kvObj);
    isaInfo = [self printMethodNamesOfClass:cls];
    NSLog(@"\n\n添加 KVO 之后:\n%@", isaInfo);
    
    // 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
    [kvObj removeObserver:self forKeyPath:@"name"];
}

日志有如下的打印:

添加 KVO 之前:
类名: KVOObject 
方法列表: .cxx_destruct, name, setName:

添加 KVO 之后:
类名: NSKVONotifying_KVOObject 
方法列表: setName:, class, dealloc, _isKVOA

说明在添加 KVO 监听之后,isa 指针的值确实是变了,具体变化为:

  • 1、将之前的 KVOObject, 更换成 NSKVONotifying_KVOObject
  • 2、在 NSKVONotifying_KVOObject 中重写了 setName:、class 与 dealloc 方法,以及添加了一个 _isKVOA 方法。

三、KVO 与 KVC 的那一份藕断丝连

关于 KVC,强烈建议看一下这篇文章KVC 的原理概述,接下来将会在这篇文章的基础上做介绍。如果不看的话,可能你很难理解我所说的 非常规 KVC 调用是什么意思。虽然,我在这里仅仅是用到了那么一丁点的内容。
首先创建一个 Class, 代码如下:

#import 

@interface KVO8KVCObject : NSObject

@end


#import "KVO8KVCObject.h"

@interface KVO8KVCObject ()
{
    // 非常规试验
    NSString* isGoddess;
}

@end

@implementation KVO8KVCObject

@end

那接下来,我们想要表达一个什么问题呢?

KVC 能否触发 KVO 监听?

看了上面的 KVO8KVCObject 定义,我即将使用一个 KVC 的非常规调用来介绍,具体代码如下:

// KVO 与 KVC 那一段藕断丝连的区域
- (void)kvo8kvc {
   // 创建一个 KVO8KVC 对象
   KVO8KVCObject* kvO_CObj = [[KVO8KVCObject alloc] init];
   
   // 通过 KVC 赋值
   [kvO_CObj setValue:@"KJ" forKey:@"goddess"];
   
   Class cls = object_getClass(kvO_CObj);
   NSString* isaInfo = [self printMethodNamesOfClass:cls];
   
   NSLog(@"\n\n添加 KVO 之前:\n%@", isaInfo);
   
   
   // 添加 KVO 监听
   [kvO_CObj addObserver:self forKeyPath:@"goddess" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
   
   // 通过 KVC 赋值
   [kvO_CObj setValue:@"JK" forKey:@"goddess"];
   
   cls = object_getClass(kvO_CObj);
   isaInfo = [self printMethodNamesOfClass:cls];
   NSLog(@"\n\n添加 KVO 之后:\n%@", isaInfo);
   
   // 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
   [kvO_CObj removeObserver:self forKeyPath:@"goddess"];
}

看一下具体的 Log 打印,如下:

添加 KVO 之前:
类名: KVO8KVCObject 
方法列表: .cxx_destruct

goddess, , {
    kind = 1;
    new = JK;
    old = KJ;
}

添加 KVO 之后:
类名: NSKVONotifying_KVO8KVCObject 
方法列表: class, dealloc, _isKVOA

可以得出结论:
KVC 能触发 KVO 监听。

看到这里,也推翻了之前的一个结论:KVO 的正常触发的入口是 setter 方法,其实不是这样的,就如同上面的这个实验,在 NSKVONotifying_KVO8KVCObject 与 KVO8KVCObject 中根本就没有其对应的 setter 方法。

四、面试题

KVO如何对集合类进行监听?

这个面试题主要针对的是一个集合类中元素变动的监听,比如一个数组如何如何监听到 添加、插入与删除。按照常规的 KVO 方式,是监听不到的,但是系统已经为我们准备了专门的 API。
具体的介绍,看一参考 InterviewKVOController 中的具体实现:

/** KVO如何对集合类进行监听?
 1. 需要借助一个 Class (KVObject), 监听这个 Class 实例对象中的集合属性
 2. 实际监听的是 mutableArrayValueForKey: 返回的集合类. 如同替身.
 */

本篇介绍,到这里就要告一段落了。在写本的时候,我有一个试验 Demo # OC2Nature,可以作为一个参考。具体请看 KVO 目录。
同时别忘了看看本专题的其它文章 OC 小专题


在之前也写过关于 KVO 的,虽然那时候理解还不是太深入,但是里面依旧是有新东西的。感兴趣的话可以去看看:

  • 1、简单的KVO实现方式: 间接的实现 KVO 的功能。
  • 2、KVO与Category :A. @property 语法在不同场景的语义以及注意事项。 B. Category中实现 KVO 监听。

你可能感兴趣的:(KVO 的进一步理解)