KVO原理

作者: 大橘猪猪侠 | 来源:发表于2020-10-28 16:00 被阅读0次

KVO通知对大多数iOS开发者来说,都不陌生;而且也是用的比较多的。
他的全称为Key-Value Observing
按照官方的解释定义:键值观察提供了一种机制,该机制允许将其他对象的特定属性的更改通知给对象。

KVO的应用

实现KVO三部曲:
1、注册通知
addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>
2、实现通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{}
3、移除通知
removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>

在注册通知时,很多人可能都不知道context的作用,它是一个上下文,按照官方的解释来说:是防止不同原因而观擦到相同的键路径导致的问题,是一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。

而当通知结束之后,我们是要将通知进行移除的,否则会导致出现NSRangeException异常;因为通知没有移除,当又一通知发送过来,将导致两个通知消息,而找不到相应的通知,会导致野指针崩溃。

手动和自动打开KVO

在有的情况下,我们不需要通知处理,而有的时候又需要通知处理,这时候就需要进行手动实现KVO了,看下面的代码:

监听属性值变化:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [Person new];
    //注册通知
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
}
//实现通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
//移除通知
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"nick" context:NULL];
}

而在Person类中,通过automaticallyNotifiesObserversForKey关闭自动通知,使用willChangeValueForKeydidChangeValueForKey来打开和关闭通知:

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}
路径处理通知

以下载为例,下载进度=已下载/总下载,当我们要监听下载进度时,需要同时关注已下载进度和总下载,那么我们可以将两个化为一个来实现监听,看代码:

首先在viewController中实现通知注册(实现和移除省略):

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

在Person类中实现两个方法:


+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

writtenDatatotalData发生改变时,就会实现通知的效果;

数组观擦

对数组内容改变进行观察,在文档中有详细介绍,KVO建立在KVC的基础上:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:  (NSKeyValueObservingOptionNew) context:NULL];
}
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"dateArray"];
}

在实现观察的NSKeyValueChangeKey属性中,有几个类型:
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,

从字面意思上就可以理解,第一个是值的设置,第二个是值的插入,第三个是删除,第四个是替换。

KVO的原理探索

首先我们来看一下KVO对成员变量和属性的监听:
在Person类中添加一个成员变量name和属性nickName

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
     [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

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

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

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

通过执行代码,发现KVO只会对属性进行观察;
那为什么KVO只对属性进行观察呢,就需要对比属性和成员变量的区别,属性多了一个setter方法,那么就可以验证得到,KVO会监听setter方法。其次会形成一个中间类(通过文档得知)。
那么如果会形成一个中间类,那么Person类的isa指向会发生变化。

看下图所示,在对person类注册通知完成后,获取self.person的类名,就得到了NSKVONotifying_Person,因此可以了解到,它有一个派生类(子类)NSKVONotifying_Person,从Person->NSKVONotifying_Person

iShot2020-10-28 15.17.12.png

下面通过一个遍历类以及子类的函数来看一下通知前后的变化:

- (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);
}


打印结果:

2020-10-28 15:21:37.858478+0800 002---KVO原理探讨[5919:438564] classes = (
    Person,
)
2020-10-28 15:21:45.950467+0800 002---KVO原理探讨[5919:438564] classes = (
    Person,
    "NSKVONotifying_Person",
)

那么就可以验证是存在一个派生类。

下面继续探索一下类的方法变化,同样的通过一个函数来查看方法变化:

- (void)printClassAllMethod:(Class)cls{
    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);
}

viewDidLoad中调用[self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];方法:

打印结果:


![iShot2020-10-28 15.40.19.png]

可以看到它有4个方法,其中dealloc_isKVOA可以理解,一个是释放,一个是判断是否为KVO类;

那么其中的setNickName方法是继承的还是重写的呢?

下面创建一个person的继承类student,打印一下student的方法,如果是继承,那么两个类打印的setNickName是一摸一样的,如果不是继承,那么就是不一样的。

student类并没有打印,因此可以得到结论NSKVONotifying_PersonsetNickName方法是重写的,应该有实现的方法。

那么既然了解到有中间类的存在,那么在中间的的执行过程中,person类的isa会指向isa,而最后是否会重新指回person类呢?

我们在析构函数处打印一下self.person的类名:

iShot2020-10-28 15.40.19.png

经过验证,在移除通知后,isa会重新指向Person类。
而经过验证,当NSKVONotifying_Person类创建之后,就会一直存在。

相关文章

网友评论

      本文标题:KVO原理

      本文链接:https://www.haomeiwen.com/subject/hlqcvktx.html