您现在的位置是:首页 >学无止境 >【iOS KVO(上)实现过程】网站首页学无止境

【iOS KVO(上)实现过程】

神奇阿道和小司 2023-07-14 16:00:02
简介【iOS KVO(上)实现过程】

前言

KVO 也适用于传值,在之前的学习只是学习了KVO的传值,今天详细学习 监听和实现

源码放在下一节学习

1.1 KVO

KVO(Key-Value Observing)是Objective-C语言中一种观察者模式的实现,可以用来监听对象属性值的变化。KVO机制允许一个对象注册为另一个对象的属性变化的观察者,并在被观察的属性值发生变化时,自动接收通知并进行相应处理。

KVO可以实现监听某个属性的变化 KVO机制只能监听对象属性值的变化,无法监听基本数据类型的变化,需要将其封装为对象属性才能实现传递。)

KVO可以实现界面之间的传值,跨界面可以。

1.2 使用KVO

现在有如下的场景
请添加图片描述
我们点击change按钮需要改变 上面的Label,同时我们需要监听Label的变化,看看如何用KVO实现键值监听

初始化按钮

- (UILabel *)test_label_init {
    if (!self.test_label) {
        self.test_label = [[UILabel alloc] init];
        self.test_label.text = @"Label not Change";
        self.test_label.font = [UIFont systemFontOfSize:25];
        [self.view addSubview:self.test_label];
        self.test_label.frame = CGRectMake(120, 170, 300, 30);
        [self.test_label addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    }
    return self.test_label;
}
- (UIButton *)test_button_init {
    if (!self.test_button) {
        self.test_button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [self.test_button setTitle:@"change" forState:UIControlStateNormal];
        [self.test_button addTarget:self action:@selector(changeLabel) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:self.test_button];
        self.test_button.frame = CGRectMake(120, 260, 100, 100);
        self.test_button.backgroundColor = [UIColor redColor];
    }
    return self.test_button;
}

注册KVO监听

通过[addObserver:forKeyPath:options:context:]方法注册KVO,这样可以接收到keyPath属性的变化事件;
请添加图片描述
observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。
keyPath:要观察的属性名称。要和属性声明的名称一致。
options:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
context:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。、

KVO监听实现

通过方法[observeValueForKeyPath:ofObject:change:context:]实现KVO的监听;
请添加图片描述

keyPath:被观察对象的属性
object:被观察的对象
change:字典,存放相关的值,根据options传入的枚举来返回新值旧值
context:注册观察者的时候,context传递过来的值

点击Button

请添加图片描述
请添加图片描述

移除KVO监听

在不需要监听的时候,通过方法[removeObserver:forKeyPath:],移除监听;

禁止KVO

我们可以手动禁止KVO监听某个属性的变化

automaticallyNotifiesObserversForKey:是一个可选的类方法,用于自定义KVO机制中属性变化通知的行为。当对象的属性发生变化时,系统会自动调用这个方法来获取是否自动发送通知。

这个方法通常被用于实现一些高级的KVO机制,比如当某个属性的变化依赖于其他属性时,可以在这个方法中检测相关属性的变化情况,从而决定是否发送属性变化通知。

我们实现一个Label的分类,在里面重写这个方法

#import "UILabel+autoCall.h"

@implementation UILabel (autoCall)
// 选择性的实现KVO
// 因为是类方法 并且改变的是Label 所以需要创建一个分类来给UILabel重写这个方法 即可完成禁止通知

// 那么也就不会实现 observeValueForKeyPath
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"text"]) {
        NSLog(@"NO WAY");
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
//    return NO;
}
@end

请添加图片描述

1.3 KVO实现监听数组内部元素变化

KVO默认只能监听到数组对象本身的变化,而无法监听到数组内部元素的变化。例如,如果将一个对象添加到数组中,KVO会收到数组count属性的变化通知,但不会收到数组内部元素的变化通知。

KVO机制只能监听对象属性值的变化,无法监听基本数据类型的变化,那么如何实现KVO监听数组内部元素的变化?

KVO(Key-Value Observing)是Objective-C语言中一种观察者模式的实现,可以用来监听对象属性值的变化。但是KVO不能直接监听数组的变化,因为NSArray和NSMutableArray并没有实现KVO机制。如果需要监听数组的变化,可以使用以下两种方式:

1.3.1 手动触发KVO通知

当数组中的元素发生变化时,手动触发KVO通知即可实现监听。具体实现方式如下:

在被观察对象的类中,重写该对象所包含的可变数组的对应方法,比如addObject:、removeObject:、insertObject:atIndex:等方法。
在重写的方法中,调用willChangeValueForKey:和didChangeValueForKey:方法,手动触发KVO通知。

- (void)addObject:(id)anObject {
    [self willChangeValueForKey:@"myArray"];
    [super addObject:anObject];
    [self didChangeValueForKey:@"myArray"];
}

在观察者中注册被观察对象的数组属性,当数组中的元素发生变化时,观察者会收到KVO通知。

[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

需要注意的是,手动触发KVO通知需要在重写的方法中手动添加代码,实现起来比较麻烦,容易出错,因此不是一种推荐的方式。

1.3.2 使用NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项

KVO支持使用NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项**,来监听可变数组中的元素变化。这两个选项会在KVO通知中包含新旧值的信息,因此可以在观察者中获取到数组中元素的变化。**

[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,根据KVO通知中的信息来处理数组元素的变化。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"myArray"]) {
        NSArray *oldArray = change[NSKeyValueChangeOldKey];
        NSArray *newArray = change[NSKeyValueChangeNewKey];
        // 处理数组元素的变化
    }
}

这种方式需要被观察的对象的数组属性必须是可变的,而且只能监听到元素的增加、删除和替换操作,

1.3.4 使用KVO的注意事项⚠️

  • keyPath 不能为空字符串
  • 注意在适合的地方removeObersver,如果观察实例比被观察实例先释放,这时候改变观察属性,会产生崩溃。
  • 没有添加,直接移除观察关系,也会产生崩溃

1.4 KVO的实现

KVO的实现原理分为3步走

1.4.1 实现

首先明确 KVO机制的实现原理是,当一个对象被观察时,系统会动态地生成一个派生类并将被观察对象的isa指针指向该派生类。这个派生类重写了被观察对象的setter方法,在setter方法中,除了进行属性值的赋值操作,还会通知观察者对象属性值的变化。

如何查看派生类?
情景:现在有一个testClass的类,里面有一个className属性,现在监听className属性

  self.testClass = [[TestClass alloc] init];
    NSLog(@"%s: isa = %s", object_getClassName(self.testClass), class_getName(object_getClass(self.testClass)));
   [self.testClass addObserver:self forKeyPath:@"className" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    NSLog(@"%s: isa = %s", object_getClassName(self.testClass), class_getName(object_getClass(self.testClass)));

其中,object_getClassName()函数可以返回对象所属的类名,object_getClass()函数可以返回对象的实际类(class),而不是对象所属的类的类型。

当这段代码执行后,就会在控制台输出该对象的类名和实际类名。实际类名即为系统动态生成的派生类名,以NSKVONotifying_为前缀,后面紧跟着被观察对象的类名。例如,如果被观察对象是一个Person对象,那么实际类名就是NSKVONotifying_Person。

注意,这种方法只适用于Objective-C的KVO机制。对于Swift的KVO机制,由于其机制不同,不能通过此方法来打印查看。请添加图片描述

如何生成派生中间类?

isa-swizzling(类指针交换):
就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。

请添加图片描述
请添加图片描述
在添加观察者之后 NSKVONotifying_testClass如何内部实现

1.4.2 NSKVONotifying_testClass如何内部实现

- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类

- (void)setClassName:(NSString *)className {

}

- (Class)class {
- 这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
    return [testClass class];
}

- (void)dealloc {
    // 收尾工作
}

- (BOOL)_isKVOA {
- 添加一个名为_isKVOA的实例变量**,用于标识该对象是否支持KVO机制。
    return YES;
}


isa指向中间类之后如何调用方法:请添加图片描述
对于这两个属性 我们只监听了className属性而没有监听testArray属性
请添加图片描述

  • 调用监听的属性的设置方法,例如:setClassName:,都会先调用NSKVONotify_testClass对应的属性设置方法
  • 调用非监听属性的设置方法,如setClassArray方法,就会通过NSKVONotify_ApplesuperClass来找到testClass类对象,在调用其Apple类对象中的test方法

重写Class方法 :这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。

添加一个名为_isKVOA的实例变量,用于标识该对象是否支持KVO机制。

1.4.3 _NSSetObjectValueAndNotify

在具体实现过程中,系统会动态生成一个继承自原始类的中间类,并且在该类的初始化方法中,调用了一个叫做_NSSetObjectValueAndNotify()的函数,用于实现属性改变的通知。

_NSSetObjectValueAndNotify()函数的实现过程如下:

a) 首先会调用 willChangeValueForKey

b) 然后给属性赋值

c) 最后调用 didChangeValueForKey

d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

1.5 总结KVO

1.5.1 KVO的本质

  • 利用runtime的API动态生成一个子类,并让实例对象的isa指向这个全新的子类
  • 当修改实例变量对象的属性时候,在全新子类的set方法中会调用Foundation的_NSSetXXXValueAndNotify函数
    willChangeValueForKey
  • 调用原来的setter
  • didChangeValueForKey:内部会触发监听器的监听方法

1.5.2 KVO使用场景

  • 对于时刻变化的对象,例如colletionView的items,总是动态的变化,这个时候可以使用KVO监听对象。
  • 在AVFounditon中获取AVPlayer的播放进度,播放状态,也需要使用KVO来观察。

1.5.3 实现过程总结

  • addObserver:forKeyPath:options:context:context调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class,并且将该对象的isa指针指向这个新的类。
  • 在该子类内部实现4个方法-被观察属性的set方法、class方法、isKVO、delloc。
  • 最关键的是set方法中,先调用willChangeValueForKey,再给成员变量赋值,最后调用didChangeValueForKeywillChangeValueForKey和didChangeValueForKey需要成对出现才能生效,在didChangeValueForKey中会去调用观察者的observeValueForKeyPath: ofObject: 方法。
  • 重写class方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass判断的时候出错。
  • isKVO方法作为能否实现KVO功能的一个标识。
  • delloc里面还原isa指针

KVO还很多内部的类和实现 先学会实现,知道实现的过程,接着学习源码和之前的实现相对应学习会更好。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。