OC动态语言特性(二): 深入理解KVO、KVC及底层实现

一 KVO

如果你看过斯坦福白胡子老爷爷的iOS视频, 那么你应该看到过下面这张图.
KVO使用
白胡子老爷爷讲述了KVO的一个重要应用, 实现在Model和Controller之间进行通信.

1 什么是KVO, 它是如何实现的

1.1 先看看官方是怎么说的

You can observe any object properties including simple attributes, to-one relationships, and to-many relationships. Observers of to-many relationships are informed of the type of change made — as well as which objects are involved in the change.
NSObject provides an implementation of the NSKeyValueObserving protocol that provides an automatic observing capability for all objects. You can further refine notifications by disabling automatic observer notifications and implementing manual notifications using the methods in this protocol.

这段话告诉我们如下几个关键信息:

  • 可以观察任何对象, 甚至简单值属性
  • 观察者观察者可以是一对一, 也可以是一对多的关系
  • NSObject对象默认实现了NSKeyValueObserving协议, 以保证这个对象可以自动观察

通过对官方文档的阅读, 我们可以看出, KVO,全称KeyValueObserving, 其实是Apple对观察者模式的一种实现方式, 是其提供的一套事件通知机制, 那么Apple是以什么方式实现的呢?

1.2 isa-swizzling 出来吧,皮卡丘
翻译成汉语, emmm 我叫它isa混写技术, 在这里我们假定你了解isa的相关技术知识!

混写其实就是Apple在运行时, 把对象的isa指针指向了另一个对象, 而你仍然认为他指向的是其类对象. 当调用addObserver: forKeyPath: options时, 系统自动的创建了一个被观察者类对象的子类, 同时把当前对象的类对象的isa指针, 指向了刚刚创建的子类, 并重写了setter方法, 最后通过监听setter方法进而监听值的改变.

[obj addObserver:observer forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:NULL];

其中, 当setter方法中didChangeValueForKey:方法调用后, 即会触发通知中心发出通知, 告知所有观察者值改变.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

原理图是这样的
在这里插入图片描述
接下来, 我们用代码验证一下是不是这样子
先创建一个被观察对象

@interface KVObject : NSObject
// 要观察的值
@property (nonatomic, assign) int value;
- (void)increase;
@end
@implementation KVObject
- (id)init {
    self = [super init];
    if (self) {
        _value = 0;
    }
    return self;
}

- (void)increase {
    _value += 1;
}

@end

然后再创建一个观察者对象

@interface KVObserve : NSObject

@end
@implementation KVObserve
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([object isKindOfClass:[KVObject class]] &&
         [keyPath isEqualToString:@"value"]) {
        // 获取value的新值
        NSNumber *valueNum = [change valueForKey:NSKeyValueChangeNewKey];
        NSLog(@"value is %@", valueNum);
    }
}
@end

最后, 检测KVO

KVObject *obj = [[KVObject alloc] init];
KVObserve *observer = [[KVObserve alloc] init];
 // 添加观察者
[obj addObserver:observer forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:NULL];

我们打断点看下结果, 当我们运行到断点位置后, 获取的被观察者是KVObject, 走一步后再获取被观察者发现变为了NSKVONotifying_KVObject, 也就是上面我们描述的子类
在这里插入图片描述

2 触发KVO的条件

我们刚刚讲过, KVO实现键值监听的本质是重写了setter方法, 换句话说, 当setter方法被调用时, 就会触发KVO监听. 我们来试一下
2.1 赋值
可以看到, 对obj.value进行赋值操作, 触发了KVO在这里插入图片描述
2.2 KVC方式修改值
仍然触发了KVO
在这里插入图片描述
2.3 对成员变量直接赋值
可以看到, 结果空空如也
空空如也
但是, 我们可以模仿底层的操作, 实现对直接修改成员变量的监听, 代码如下

- (void)increase {
    // 模拟KVO
    [self willChangeValueForKey:@"value"];
    _value += 1;
    [self didChangeValueForKey:@"value"];
}

此时, 成员变量也成功被监听, 因为KVO的本质是监听setter方法, 我们实现了didChangeValueForKey后, 达到了触发键值监听的条件.
在这里插入图片描述

二 KVC

1 KVO的两个方法

KVC, 即key value coding, 键值编码技术

 - (void)setValue:(nullable id)value forKey:(NSString *)key;
 - (id)valueForKey:(NSString *)key;

我们先看看这两个api的系统调用流程

  • 通过key, 寻找是否有getter方法/setter方法, 如果有, 直接调用, 返回结果
  • 如果没有, 则寻找实例变量是否存在, 如果有也是直接调用, 返回结果
  • 如果实例变量不存在, 会走系统默认的处理流程抛出异常(未定义key)

两个方法的流程图如下:
1.1 valueForKey:
在这里插入图片描述
1.2 setValue: forKey:
在这里插入图片描述
需要注意的是(有兴趣的同学, 可以分别重写上述几个方法, 然后注意打印具体调用实现):
在寻找Accessor Method时, 内部会分别以三种方式尝试获取值的, 包括:

  • getKey
  • key
  • isKey

在寻找Instance var时, 内部会分别以以下四种方式尝试获取值, 包括:

  • _key
  • _isKey
  • key
  • isKey

2 KVO和面向对象

Apple键值编码指南中有如下这段描述:

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.
You typically use accessor methods to gain access to an object’s properties. A get accessor (or getter) returns the value of a property. A set accessor (or setter) sets the value of a property. In Objective-C, you can also directly access a property’s underlying instance variable. Accessing an object property in any of these ways is straightforward, but requires calling on a property-specific method or variable name. As the list of properties grows or changes, so also must the code which accesses these properties. In contrast, a key-value coding compliant object provides a simple messaging interface that is consistent across all of its properties.

Apple告诉你, KVC是一种非正式协议, 你可以通过KVC间接操作访问对象, 通过访问器(getter)方法访问属性的值, 通过设置器(setter)方法修改属性的值. 他还特意强调了一下, 你可以"直接"访问"任何一种"属性, 成员变量, 简单属性等;

我的理解是, 当知道某个类的某个属性或者私有成员变量时, 那么在外界可以直接通过KVC访问它们, 进行读写操作;以下是我个人的理解, 如有错误, 万望大佬指正, 不胜感激!

面向对象的特征之一是封装,
封装的概念是: 把客观事物抽象成具体的类, 并且类可以把自己的数据和方法只允许给可信的类或者对象操作, 对不可信的信息进行隐藏!

然而, KVC可以直接通过key暴力访问, 破坏了面向对象的封装思想.

我是个写代码的小学生, 如有不足, 万望不吝指教

参考资料:
Apple键值编码指南
Apple键值观察指南

发布了4 篇原创文章 · 获赞 1 · 访问量 138

猜你喜欢

转载自blog.csdn.net/weixin_43837354/article/details/104659965