07-KVO的底层分析

OC底层原理探索文档汇总

主要内容:
KVO的使用
KVO的底层实现

查阅KVO官方文档

key-value-observing(键值观察)
简单来说就是通过一个key来找到某个属性并监听其值的改变,KVC是实现KVO的基础,因为需要键值监听,KVO只能实现属性的监听,也就是有setter方法的监听,其他的比如成员变量是不行的。

KVO的简单使用

普通的属性设置

  1. 添加观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];

参数:

  • observer:观察者,通常是自己,也可以是任何人
  • keyPath:观察的内容,也就是属性名,这里写成字符串的形式(由此也可以知道KVO是基于KVC的)
  • option:
    • 响应类型,也就是在何种情况下会进行响应,决定了通知中的内容和发送时间。
    • 常用的就是NSKeyValueObservingOptionNew,也就是更新属性值
  • context:
    • 表示上下文环境,在观察响应时传递一些额外的信息
    • 一般可以用它来判断观察的属性是哪个
    • context不能用nil填充,因为是viod *,所以需要用null
    • context为了区分每一次观察的细节,不直接通过对象和keyPath来判断,这样比较麻烦,因为还需要进行大量对象的判断,这样写起来复杂又麻烦,用context可以性能好,而且可读性好

说明:

  • 把person的nick属性添加观察者,观察者就是当前对象
  • context用来传递额外信息

option:

NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)

  1. 在观察者中实现监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context{

参数:
keyPath:监听的属性
object:被监听者
change:表示数据修改的方式:
context:添加监听时的上下文参数

说明:

  • 这个方法是一个是系统会调用的,当观察到属性发生变化时就自动调用
  • 它是针对属性的监听后进行的响应
  • NSKeyValueChangeKey用来设置观察到属性改变的方式,比如给数组插入数据,比如给一个属性更新一个数据

NSKeyValueChangeKey类型

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//设值
    NSKeyValueChangeInsertion = 2,//集合插入值
    NSKeyValueChangeRemoval = 3,//集合移除值
    NSKeyValueChangeReplacement = 4,//集合替换值
};
  1. 移除观察者
[a removeObserver:b forKeyPath:];

说明:

  • 一般都在dealloc,也就是销毁该对象的时候
  • 一定要移除的,不移除就会崩溃

为什么不移除观察者会崩溃:

  • 如果没有移除,当被观察者的属性发生改变时,会通知查找观察者,而如果观察者已经被注销掉了,就无法通知到位,野指针异常了

属性依赖

如果我们想要监听一个属性,但是这个属性的变化不是直接修改的,而是通过其他几个属性的修改来影响的,这就是属性依赖。
也就是说我们通过监听其他值,响应目标值。两个值的观察变成一个值的观察,观察到两个值中只要有一个进行变化,即进行结果返回。

其他的都和普通的实现一样,重点在于keyPathsForValuesAffectingValueForKeyAPI的使用

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    //判断是真正要监听的那个属性
    if ([key isEqualToString:@"downloadProgress"]) {
        //添加keyPaths,这个数组里的每个属性发生变化都会响应观察
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  • 通过方法系统提供的类方法keyPathsForValuesAffectingValueForKey来实现的,最终返回keyPaths,而这个keyPaths里放的就是需要监听的那几个属性
  • 当这个属性添加观察后,KVO会自动检测Set中的所有keyPaths

数组的设置

重点需要注意数组元素改变和数组本身的改变是不一样的,对于集合类型的操作一定要注意我们操作的方式,这里需要用到KVC来观察数组属性的改变。

代码:

// 5: 数组观察
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    //观察者数组的KVO,必须利用KVC的原理机制才可以观察到
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

说明:

  • 对于数组的观察,需要使用KVC来进行设置才可以监听到

手动和自动设置开关

系统默认支持KVO的通知发送,同时系统也提供了一个入口,让我们可以手动去设置是否需要支持发送,如果对这个类关闭了自动开关后,还可以针对其中的某个属性进行手动开启。
手动设置开关可以让我们灵活的加上自己想要的判断条件

代码:

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
//    return YES;//自动
    return NO;//手动,自己写setter
}
//这两个方法都是系统自动监听时要使用的方法,如果不自动监听,就需要我们自己加了。
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

说明:

  • 重写这个方法,如果返回YES,就说明是自动的,如果返回NO,是手动的
  • 如果需要对某个属性进行观察,可以在其setter方法中添加willChangeValueForKey和didChangeValueForKey方法

小结:

  1. KVO的使用都是三步曲,添加观察者,观察响应,移除观察者
  2. 我们可以自己控制是否需要进行观察
  3. 对于数组的观察需要使用KVC进行设值
  4. 通过属性依赖,可以对有依赖性质的属性进行观察

KVO的底层实现

核心就是新建了一个中间类,是当前类的子类,在setter方法中增加了两个方法,这样就可以进行观察了。

底层的实现过程

底层实现图.png
  • 在添加观察后,系统会动态的生成一个新的中间类
  • 中间类中有被观察的属性的setter方法
  • setter方法包含三条件语句
    • 1、通知观察者值即将改变[self willChangeValueForKey:@""];
    • 2、修改值[super setValue: forKey:];
    • 3、通知观察者值已经改变[self didChangeValueForKey@""];

注意:

  • 观察的只是属性,不能观察成员变量,因为观察属性其实也观察的是setter方法
  • 重新创建一个当前类的子类,这个子类的命名是这样的:NSKVONotifying_LGPerson
  • 移除观察后,不会删除这个中间类,只要注册到内存中,就会一直存在
  • 创建了中间类后,对象的isa会变成中间类,也就是我们观察的对象会指向中间类
    • 这里值得注意的是此时这个对象通过class返回的仍然是原来的类
    • 移除观察后还会指回之前的类

你可能感兴趣的:(07-KVO的底层分析)