详解KVO 一、基本概念 二、基本用法 三、高级用法 四、关于NSKVONotifying_原类型名(简称此类为中间类)

一、基本概念

        KVO的全称是Key-value observing(键值观察),它提供了一种机制,允许对象在其他对象的特定属性发生变化时收到通知。

二、基本用法

// MyTestClass.h

@interface MyTestClassBase : NSObject
@end

@protocol ATextProtocol <NSObject>
- (void)notImpFun;

@end

@interface MyTestClass : MyTestClassBase <ATextProtocol>
- (void)printClassObject;
+ (void)printClassClass;

@property (nonatomic, assign) BOOL childProperty;

@end

// MyTestClass.m

#import "MyTestClass.h"
#import <objc/runtime.h>

@implementation MyTestClassBase


@end

@implementation MyTestClass

- (void)printClassObject {
    NSLog(@"%@ printClassObject", NSStringFromClass(self.class));
    
    Class cls = self.class;
    while (cls != Nil) {
        NSLog(@"object =%@ class =%@", self, NSStringFromClass(cls));
        cls = class_getSuperclass(cls);
    }
}


+ (void)printClassClass {
    NSLog(@"MyTestClass printClassClass");
    
    Class cls = self;
    while (cls != Nil) {
        NSLog(@"class =%@", NSStringFromClass(cls));
        cls = class_getSuperclass(cls);
    }
}

- (void)setChildProperty:(BOOL)childProperty {
    _childProperty = childProperty;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    Class cls = self;
    while (cls != Nil) {
        NSLog(@"resolveInstanceMethod class=%@ sel=%@", NSStringFromClass(cls), NSStringFromSelector(sel));
        cls = class_getSuperclass(cls);
    }
    
    return [super resolveInstanceMethod:sel];
}
@end
// ViewController.m

@interface ViewController ()

@property (nonatomic, strong) MyTestClass *child;

@property (nonatomic, strong) UIButton *button;
@property (nonatomic, assign) BOOL tap;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    _child = [MyTestClass new];
    [_child printClassObject];
    [MyTestClass printClassClass];
    
    _button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 120, 60)];
    [_button setTitle:@"添加KVO" forState:UIControlStateNormal];
    [_button setTitleColor:[UIColor blueColor]  forState:UIControlStateNormal];
    [_button addTarget:self action:@selector(tap:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_button];
}

- (void)tap:(id)sender {
    [_child addObserver:self forKeyPath:@"childProperty" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    _child.childProperty = YES;
    _tap = YES;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"childProperty"]) {
        NSLog(@"%@", change);
    }
    
    [_child printClassObject];
    [MyTestClass printClassClass];
    
//    [_child notImpFun];
}

- (void)dealloc {
    if (_tap) {
        [_child removeObserver:self forKeyPath:@"childProperty"];
    }
}

2.1 基本用法

// 首先需要通过被观察对象调用addObserver,把观察者添加到被观察对象上
// NSKeyValueObservingOptions 指定了能够获取值的类型
// context是观察者与被观察之之间通信的上下文,其用于复杂场景
[_child addObserver:self forKeyPath:@"childProperty" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];


// 然后观察者对象实现observeValueForKeyPath即可
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

2.2 移除观察者

        虽然在iOS9之后KVO就不需要手动移除,但是推荐还是手动移除(如果觉得Apple原生的KVO使用麻烦——确实听麻烦的,可以使用一些三方库,他们一版会自动移除观察者)。
        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.
        以上是官方的说法,大意是:解除分配时,观察者不会自动删除自己。 被观察的对象继续发送通知,而忽略了观察者的状态。 但是,发送到已释放对象的更改通知与任何其他消息一样,会触发内存访问异常。 因此,您要确保观察者在从内存中消失之前将自己移除

2.3 如何触发observeValueForKeyPath(这里是初步解释,后续有详细说明)

        其实很简单,iOS运行时在背后做了一些手脚。为了更清楚观察具体触发过程,请添加如下代码。

// 在MyTestClass.m中为MyTestClass添加如下代码
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    NSLog(@"automaticallyNotifiesObserversForKey");
    return YES; // 这里返回YES,则KVO会自动触发,如果返回NO,则需要手动触发
}
  1. 在VC的Tap事件中设置断点,并执行p *self.child命令,可以看出child的类型是MyTestClass


  2. 注册观察者——此时需要通过调用automaticallyNotifiesObserversForKey方法以获取是否是自动触发的信息


  3. 自动修改对象类型——把child由MyTestClass修改为NSKVONotifying_为前缀的NSKVONotifying_MyTestClass类型



  1. 修改被观察者的属性值


  2. 通知观察者


2.4 梳理与总结:

  1. 添加观察者时,系统把对象的类型修改为NSKVONotifying_原类型名,这个新类继承自原类型
  2. 添加观察者时,系统调用automaticallyNotifiesObserversForKey,询问是否自动触发被观察者
  3. 修改被观察者的属性
  • 调用willChangeValueForKey:
  • 调用原来类的setter方法([super setAge:age])
  • 调用didChangeValueForKey:
  • didChangeValueForKey:内部会调用observer的observerValueForKeyPath:ofObject:change:context:方法
  1. 观察者获得通知

三、高级用法

3.1 手动触发观察者

[_child willChangeValueForKey:@"childProperty"]; 
_child.childProperty = YES;
[_child didChangeValueForKey:@"childProperty"]; 

3.2 观察多个属性

        当有时某个属性依赖于其他多个属性时,对当前属性的观察相当于需要观察多个属性时,如何处理?参加以下代码。

// MyTestClass.h,为MyTestClass添加以下属性

@property (nonatomic, assign) NSNumber *mainProperty; // 被观察的属性

@property (nonatomic, assign) NSNumber *subProperty1; // 被mainProperty 依赖
@property (nonatomic, assign) NSNumber *subProperty2; // 被mainProperty 依赖

// MyTestClass.m,添加以下方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"mainProperty"]) {
        NSArray *affectingKeys = @[@"subProperty1", @"subProperty2"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSNumber *)mainProperty {
    return [NSNumber numberWithInteger:_subProperty1.integerValue + _subProperty2.integerValue];
}
// ViewController.m,观察child的mainProperty

- (void)tap:(id)sender {
    [_child addObserver:self
             forKeyPath:@"mainProperty"
                options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                context:nil];
    _child.subProperty1 = @1;
    _child.subProperty2 = @2;
    _tap = YES;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"mainProperty"]) {
        NSLog(@"%@", change);
    }
    
    [_child printClassObject];
    [MyTestClass printClassClass];
    
//    [_child notImpFun];
}

/ *     _child.subProperty2 = @1; 的输出结果
2021-12-01 10:35:44.693231+0800 StudyKVO[13682:158801] {
    kind = 1;
    new = 1;
    old = 0;
}
*/ 

/ *     _child.subProperty2 = @2; 的输出结果
2021-12-01 10:35:44.693231+0800 StudyKVO[13682:158801] {
    kind = 1;
    new = 3;
    old = 1;
}
*/ 

3.3 观察集合类型

// MyTestClass.h
@property (nonatomic, strong) NSMutableArray *dataArray;

// MyTestClass.m
- (NSMutableArray*)dataArray {
    if (!_dataArray) {
        _dataArray = [NSMutableArray array];
    }
    return _dataArray;
}

// ViewController.m
- (void)tap:(id)sender {
    [_child addObserver:self
             forKeyPath:@"dataArray"
                options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                context:nil];
    
    [[self.child mutableArrayValueForKey:@"dataArray"] addObject:@1];
    _tap = YES;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"dataArray"]) {
        NSLog(@"%@", change);
    }
    
    [_child printClassObject];
    [MyTestClass printClassClass];
    
//    [_child notImpFun];
}

四、关于NSKVONotifying_原类型名(简称此类为中间类)

4.1 基本内容

        被观察者的对象的isa指针执行了一个运行时自动生成的类NSKVONotifying_原类型名,此类继承自被观察者的原始类型。下面对此类进行分析。

2021-12-01 11:25:59.921673+0800 StudyKVO[16739:209645]  cls NSKVONotifying_MyTestClass : fun setMyCustomType: --- imp 0x7fff207a3203
2021-12-01 11:25:59.921824+0800 StudyKVO[16739:209645]  cls NSKVONotifying_MyTestClass : fun class --- imp 0x7fff207a1d0d
2021-12-01 11:25:59.921939+0800 StudyKVO[16739:209645]  cls NSKVONotifying_MyTestClass : fun dealloc --- imp 0x7fff207a1abd
2021-12-01 11:25:59.922053+0800 StudyKVO[16739:209645]  cls NSKVONotifying_MyTestClass : fun _isKVOA --- imp 0x7fff207a1ab5
2021-12-01 11:26:00.373625+0800 StudyKVO[16739:209645] ----------
2021-12-01 11:26:01.282659+0800 StudyKVO[16739:209645]  cls MyTestClass : fun printClassObject --- imp 0x102476830
2021-12-01 11:26:01.282837+0800 StudyKVO[16739:209645]  cls MyTestClass : fun setChildProperty: --- imp 0x1024769f0
2021-12-01 11:26:01.282958+0800 StudyKVO[16739:209645]  cls MyTestClass : fun mainProperty --- imp 0x102476c50
2021-12-01 11:26:01.283111+0800 StudyKVO[16739:209645]  cls MyTestClass : fun childProperty --- imp 0x102476cd0
2021-12-01 11:26:01.283262+0800 StudyKVO[16739:209645]  cls MyTestClass : fun setMainProperty: --- imp 0x102476d00
2021-12-01 11:26:01.283395+0800 StudyKVO[16739:209645]  cls MyTestClass : fun subProperty1 --- imp 0x102476d30
2021-12-01 11:26:01.283551+0800 StudyKVO[16739:209645]  cls MyTestClass : fun setSubProperty1: --- imp 0x102476d50
2021-12-01 11:26:01.283651+0800 StudyKVO[16739:209645]  cls MyTestClass : fun subProperty2 --- imp 0x102476d80
2021-12-01 11:26:01.283735+0800 StudyKVO[16739:209645]  cls MyTestClass : fun setSubProperty2: --- imp 0x102476da0
2021-12-01 11:26:01.283814+0800 StudyKVO[16739:209645]  cls MyTestClass : fun myCustomType --- imp 0x102476e10
2021-12-01 11:26:01.283906+0800 StudyKVO[16739:209645]  cls MyTestClass : fun setMyCustomType: --- imp 0x102476e30
2021-12-01 11:26:01.284014+0800 StudyKVO[16739:209645]  cls MyTestClass : fun .cxx_destruct --- imp 0x102476e70
2021-12-01 11:26:01.284093+0800 StudyKVO[16739:209645]  cls MyTestClass : fun dataArray --- imp 0x1024768f0
2021-12-01 11:26:01.290066+0800 StudyKVO[16739:209645]  cls MyTestClass : fun setDataArray: --- imp 0x102476dd0

--------

  1. _isKVOA:是一个辨识码,来判断这个类是不是因为KVO产生的动态子类
  2. dealloc:判断它是否进行释放
  3. class:是类的信息
  4. setMyCustomType:是要变化的属性的setter方法
  5. 在dealloc中移除观察者后,对象的isa就变回原有类型

4.2 总结分析

  • 在添加观察时,runtime会产生一个中间类:
    • 中间类继承于原类
    • 中间类会重写被观察key的setter方法,
    • 对象的isa从指向元类,变成指向中间类
  • 当对属性赋值时,对象会根据isa找到中间类对应的setter方法,然后在willChangeValueForKey和didChangeValueForKey方法之间进行赋值,进而触发-(void)observeValueForKeyPath:ofObject:change:context:方法。
  • 当在dealloc中移除通知后,isa会重新指向原来的类,相关实例变量的值不变。dealloc后中间类并不会释放,依然在注册类中。

4.3 最后的说明

        如果你那第二节中的代码进行测试,就会发现在即使在添加观察之后,在VC的代码中po _child.class依然输出的是MyTestClass,但是在MyTestClass的resolveInstanceMethod方法中,获取的class确是NSKVONotifying_MyTestClass。如果你基于类名MyTestClass,在resolveInstanceMethod进行某些处理,那么可能会忽略此错误。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章