Objective-C学习篇第十二弹:键值观察KVO详解

Introduction to Key-Value Observing Programming Guide

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

Important: In order to understand key-value observing, you must first understand key-value coding.

一、KVO简介

KVO 全称 Key-Value Observing。中文叫键值观察。KVO其实是一种观察者模式,观察者在键值改变时会得到通知,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。相比Notification,KVO更加的简单直接。

KVO的操作方法由NSKeyValueCoding提供,而他是NSObject的类别,也就是说ObjC中几乎所有的对象都支持KVO操作。

二、运用键值观察
KVO的使用就是简单的三步曲:

1、注册需要观察的对象的属性
如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察者)上调用名为NSKeyValueObserverRegistration 的category方法将观察者对象与被观察者对象注册

 - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

2、 处理变更通知
观察者需要实现名为 NSKeyValueObserving 的 category 方法来处理收到的变更通知:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

实现observeValueForKeyPath:ofObject:change:context:方法,这个方法当观察的属性变化时会自动调用.在这个方法中还通过NSKeyValueObservingOptionNew这个参数要求把新值在dictionary中传递过来。

3、取消注册需要观察的对象

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

示例程序:
main.m

// key - valued - observer 键值观察

//1、注册被观察者
//2、观察者接收被观察者值的变化
//3、不使用时一定记得remove掉

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
#import "Watcher.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person * p = [[Person alloc] init];
        Watcher * w = [[Watcher alloc] init];
        p.watcher = w;
        p.age = 20;
        NSLog(@"%@",p);

        [p registerObserver];
        p.age = 12;
        NSLog(@"%@",p);
        p.age = 30;
    }
    return 0;
}

Person.h

#import <Foundation/Foundation.h>
#import "Watcher.h"

@interface Person : NSObject

@property (nonatomic, assign) int age;

@property (nonatomic, strong) Watcher * watcher;

- (void)registerObserver;

@end

Person.m

#import "Person.h"

@implementation Person

- (NSString *)description {

     return [NSString stringWithFormat:@"%s", object_getClassName(self)];
}

- (void)registerObserver {

    [self addObserver:self.watcher forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

- (void)dealloc {

    [self removeObserver:self.watcher forKeyPath:@"age"];
}

@end

Watcher.h

#import <Foundation/Foundation.h>

@interface Watcher : NSObject

@end

Watcher.m

#import "Watcher.h"

@implementation Watcher

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {

    if ([keyPath isEqualToString:@"age"]) {

        NSLog(@"%@",change[@"new"]);

    } else {

        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

运行结果:
这里写图片描述

思考,当被观察者属性重写设置,但值并没有改变,每次设置都会触发KVO,如果有多次设置重复设置相同的数值呢?那么需要手动设置KVO。

三、手动实现键值观察KVO
示例程序:
main.m

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
#import "Watcher.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person * p = [[Person alloc] init];
        Watcher * w = [[Watcher alloc] init];
        p.watcher = w;
        p.age = 20;
        NSLog(@"%@",p);

        [p registerObserver];
        p.age = 12;
        NSLog(@"%@",p);
        p.age = 12;
    }
    return 0;
}

Person.h

#import <Foundation/Foundation.h>
#import "Watcher.h"

@interface Person : NSObject

//KVO 需要实现setter/getter方法
@property (nonatomic, assign) int age;

@property (nonatomic, strong) Watcher * watcher;

- (void)registerObserver;

@end

Person.m

#import "Person.h"

@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {

    BOOL automatic = YES;
    if ([key isEqualToString:@"age"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }

    return automatic;
}

- (void)setAge:(int)age {

    //手动设置KVO

    if (_age != age) {

        [self willChangeValueForKey:@"age"];

        _age = age;

        [self didChangeValueForKey:@"age"];

    }
}

- (NSString *)description {

    return [NSString stringWithFormat:@"%s", object_getClassName(self)];
}

@end

Watcher.h

#import <Foundation/Foundation.h>

@interface Watcher : NSObject

@end

Watcher.m

#import "Watcher.h"

@implementation Watcher

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {

    if ([keyPath isEqualToString:@"age"]) {

        NSLog(@"%@",change[@"new"]);

    } else {

        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

执行结果:
这里写图片描述

首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;

其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

四、键值观察实现原理:

键值观察用处很多,Core Binding 背后的实现就有它的身影,那键值观察背后的实现又如何呢?想一想在上面的自动实现方式中,我们并不需要在被观察对象 Target 中添加额外的代码,就能获得键值观察的功能,这很好很强大,这是怎么做到的呢?答案就是 Objective C 强大的 runtime 动态能力,下面我们一起来窥探下其内部实现过程。

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

简单的说主要是因为KVO的实现使用了isa-swizzling。在程序运行时Person会生成一个派生类NSKVONotifying_Person,在这个派生类中重写基类中任何被观察属性的setter方法,用来欺骗系统顶替原先的类。在setter方法中实现真正的通知机制.

苹果官方给出的原理简介:

这里写图片描述

苹果官方KVO文档:

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值