KVO原理探究

247 阅读10分钟

一、KVO的使用

上一篇我们介绍了KVC,并在在KVC中我们设计到了KVO方面东西,现在我们来了解一下KVO,首先我们先了解一下KVO的使用

1、常规使用

注册观察者,注意context可以填写NULL,而不是nil,因为context类型是context:(nullable void *)context,是void *类型,所以填写的是NULL

    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

context是通过官方文档,我们发现是为了避免观察者观察不同对象的相同的keyPath导致不好区分而设置的,例如可以按照下面方式: 定义void *

static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext;

添加观察者

    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext];

观察代理:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"LGViewController - %@",change);

    if (context == StudentNameContext) {

    }else if (context == PersonNameContext){

    }
}

当观察者注销后需要将观察者移除观察,否则的话会崩溃

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}

2、业务的增加和删除

当我们在实际开发者,可能业务需求会返回对一个属性进行监听,这样就会导致反复删除增加代码,这样就对代码很不又有,我们可以在被观察对象的类中增加一个自动开关:

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    //这样针对该类对象的name属性就不能自动监听了
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

那么有自动监听,就有手动监听了,当自动监听移除时候,我们就能用手动监听方式继续发送通知了:

    [self.person willChangeValueForKey:@"name"];
    self.person.name  = @"null";
    [self.person didChangeValueForKey:@"name"];

3、多个因素影响

有时候我们监听一个属性时候,可能涉及到其他的多个属性,例如下载比例会涉及到总量和下载量等,这个时候我们可以在被监听的对象类中实现下面方法:

// 下载进度 -- writtenData/totalData
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

然后监听downloadProgress属性

    [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

这样会使,如果totalData、writtenData任意一个属性发生变化时候就会触发回调

4、可变数组 的监听

假如一个属性类型是可变数组,那么当可变数组里面元素发生变化时候怎么触发回调呢,这个需要特殊处理一下:

    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];

这个时候在回调时候就能接收到通知:

2020-02-19 00:04:43.694990+0800 001---KVO初探[12554:3323735] LGViewController - {
    indexes = "<_NSCachedIndexSet: 0x600001cd15e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        2
    );
}

但是这里我们看到回调的change里面的new紧紧是新增加的元素,而不是整个数组,这也是KVO建立在KVC上的一个原因。 并且NSSet跟NSArray一样

二、KVO原理

首先我们先看一下官方文档对KVO的描述KVO官方文档

官方文档描述,KVO其实就是生成一个中间类,并且将观察对象的isa进行修改成中间类,下面我们来验证一下:

1、动态生成子类

先在控制器中写下下面代码:

    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];

并且在这句代码前后都打上一个断点,然后通过LLDB来查看一下self.person的isa在注册KVO前和注册后的区别

我们发现注册KVO后self.person的类变成了NSKVONotifying_LGPerson,但是我们不确定是动态生产了一个类还是把原来的LGPerson改成NSKVONotifying_LGPerson,我们再验证一下

我们发现在注册前后LGPerson的name没变,所以确定是系统生成了动态类NSKVONotifying_LGPerson,然后将self.person的isa改成了动态类NSKVONotifying_LGPerson

但是动态类NSKVONotifying_LGPerson和LGPerson的关系是什么呢,我们来检测一下

首先我们定义一个方法,用来遍历一个类的所有子类

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

然后在控制器中写下面测试代码,来观察注册KVO前和注册KVO后的区别

    [self printClasses:[LGPerson class]];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClasses:[LGPerson class]];

打印结果

2020-02-19 14:16:38.586589+0800 002---KVO原理探讨[74528:4394696] classes = (
    LGPerson,
    LGStudent
)
2020-02-19 14:16:38.591254+0800 002---KVO原理探讨[74528:4394696] classes = (
    LGPerson,
    "NSKVONotifying_LGPerson",
    LGStudent
)

我们发现注册KVO后LGPerson多了一个子类NSKVONotifying_LGPerson,说明NSKVONotifying_LGPerson是LGPerson的子类。

2、观察的是setter

我们知道KVO本质上是属性监听,下面我们来验证一下,首先给LGPerson一个属性nickName,和一个成员变量name(记得怼成员变量name用public描述一下,否则在类外无法操作)

@interface LGPerson : NSObject
{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

然后对两个进行监听

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];

再进行修改值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"KC";
    self.person->name    = @"Cooci";
}

我们发现在监听回调里面只有nickName,而没有name

而属性和成员变量的区别就是是否有setter/getter方法

我们再进一步验证,在在LGPerson中添加一个setName方法

-(void)setName:(NSString *)names{
    name = names;
}

然后再做一下测试:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nickName = @"KC";
    self.person.name    = @"Cooci";
}

打印:

2020-02-19 17:50:18.065052+0800 002---KVO原理探讨[86721:4673179] {
    kind = 1;
    new = KC;
}
2020-02-19 17:50:18.065352+0800 002---KVO原理探讨[86721:4673179] {
    kind = 1;
    new = Cooci;
}

发现打印了,我们再把name这个成员变量去掉再试试

发现崩溃了,而且是上一篇说的KVC找不到对应key的错误:valueForUndefinedKey,

经上面验证,我们知道KVO,其实就是对set方法进行监听,并且在监听过程中调用了KVC的取值方法,所有只要被观察对象有成员变量对应的set方法,那么就能通过KVO对该对象进行观察

3、子类的操作

我们再来探究下动态生成的子类做了什么操作,我们知道类中的数据主要是方法、属性、成员变量等,但考虑到KVO是对属性进行监听,所以我们先看一下动态生成的子类中的方法有写哪些,先写一个方法获取一个类中的所有method:

//打印出类中的所有方法
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

我们写一下测试代码:


    [self printClassAllMethod:LGPerson.class];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];

打印结果:

2020-02-19 15:43:12.343082+0800 002---KVO原理探讨[79179:4503930] *********************
2020-02-19 15:43:12.343292+0800 002---KVO原理探讨[79179:4503930] .cxx_destruct-0x10a525590
2020-02-19 15:43:12.343439+0800 002---KVO原理探讨[79179:4503930] nickName-0x10a525520
2020-02-19 15:43:12.343545+0800 002---KVO原理探讨[79179:4503930] setNickName:-0x10a525550
2020-02-19 15:43:12.344137+0800 002---KVO原理探讨[79179:4503930] *********************
2020-02-19 15:43:12.344298+0800 002---KVO原理探讨[79179:4503930] setNickName:-0x7fff25721c7a
2020-02-19 15:43:12.344412+0800 002---KVO原理探讨[79179:4503930] class-0x7fff2572073d
2020-02-19 15:43:12.344508+0800 002---KVO原理探讨[79179:4503930] dealloc-0x7fff257204a2
2020-02-19 15:43:12.344670+0800 002---KVO原理探讨[79179:4503930] _isKVOA-0x7fff2572049a

我们发现,动态生成的子类里面有四个方法:setNickName、class、dealloc、_isKVOA,即使我们对name进行了监听,子类里面也没有setName方法

这个地方我们注意下,子类的方法是子类的,虽然子类能调用父类的方法,但是那是属于父类的方法

我们一个个对方法进行分析

  • 1、setNickName:我们知道KVO其实就是对属性的setter方法进行监听,所以在子类中重写了父类属性的set方法
  • 2、class方法:由于动态生成的子类是系统在后台生成的,并且是自己将观察对象的isa执行动态的子类,开发者是不知道的,为了不影响开发者对原来观察对象的使用,会重写class,然后返回动态生成子类的父类,也就是观察者对象本身的类
  • 3、dealloc:为了释放

4、移除观察后 isa 是否指回来

我们做一下测试:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    [self.person removeObserver:self forKeyPath:@"nickName"];

}

然后加一个断点看看

我们发现在移除所有的KVO观察后,self.person的isa改回去了

5、移除观察后,子类是否注销

我们还有一个考虑,当移除观察者后,动态创建的子类是否会注销掉呢?我们再做一下测试:

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"nickName"];
    [self printClasses:[LGPerson class]];
}

打印结果:

2020-02-19 16:45:36.350749+0800 002---KVO原理探讨[82992:4585381] classes = (
    LGPerson,
    LGStudent,
    "NSKVONotifying_LGPerson"
)

说明动态生成的子类未注销,这是为啥呢,因为动态生成类对CPU的消耗很大,且该对象并不是移除完成后,在后面操作过程中就不会再观察了,假如会注销,在APP生命周期内可能会反复生成、反复注销,所以在观察移除后,动态生成色子类不会注销

6、KVO原理总结

  • 1、当对对象A进行KVO观察时候,会动态生成一个子类,然后将对象的isa指向新生成的子类
  • 2、KVO本质上是监听属性的setter方法,只要被观察对象有成员变量和对应的set方法,就能对该对象通过KVO进行观察
  • 3、子类会重写父类的set、class、dealloc方法
  • 4、当观察对象移除所有的监听后,会将观察对象的isa指向原来的类
  • 5、当观察对象的监听全部移除后,动态生成的类不会注销,而是留在下次观察时候再使用,避免反复创建中间子类

三、自定义KVO

待续