iOS-KVO(一) 基本操作

iOS-KVO(一) 基本操作
iOS-KVO(二) 使用注意点
iOS-KVO(三) 窥探底层实现
iOS-KVO(四) 自定义KVO+Block

KVO(key-value-observing)是实现键值观察的方式,当某个属性的值发生变化的时候,通知观察者;
KVO本质上其实是一个观察者模式;
一般继承自NSObject的对象都默认支持KVO;

(1)使用KVO的三个步骤

  1. 注册Observer;
  2. 接收属性值的变化;
  3. 移除Observer;

先介绍以下每个步骤对应的方法,以及对应的参数的含义

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

对应的参数:
observer:观察者,监听属性变化的对象,该对象必须实现observeValueForKeyPath:ofObject:change:context:方法;
keyPath:要观察的属性名称;
options:调用接收方法的时机以及包含的内容;
context:上下文,可以传入任意类型的对象,将在消息回调的时,可以接收到这个对象,是KVO的一种传值方式;
其中 options参数:

     typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew = 0x01,
        NSKeyValueObservingOptionOld = 0x02,
        NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
        NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
     };

     NSKeyValueObservingOptionNew:接收方法的change参数的中包含新的值(NSKeyValueChangeNewKey)
     NSKeyValueObservingOptionOld:接收方法的change参数的中包含旧的值(NSKeyValueChangeOldKey)
     NSKeyValueObservingOptionInitial:注册的时候发一次通知,改变后也发送一次通知
     NSKeyValueObservingOptionPrior:属性改变之前发一次,改变之后再发一次,变化前的通知change参数包含notificationIsPrior = 1

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

对应的参数:
keyPath:被观察的属性
object:被观察的对象
change:在注册时用options参数进行的配置,会包含不同的内容
context:注册的上下文

其中change的key(NSKeyValueChangeKey)有以下几种

  • NSKeyValueChangeKindKey:对应着四个修改的类型,其中后面三个是集合对象的操作方式
    NSKeyValueChangeSetting = 1, 赋值
    NSKeyValueChangeInsertion = 2, 插入
    NSKeyValueChangeRemoval = 3, 移除
    NSKeyValueChangeReplacement = 4, 替换

  • NSKeyValueChangeNewKey:新值

  • NSKeyValueChangeOldKey:旧值

  • NSKeyValueChangeIndexesKey

  • NSKeyValueChangeNotificationIsPriorKey

iOS-KVO(一) 基本操作_第1张图片
NSKeyValueChangeKindKey.png
移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

对应的参数:
observer:要移除的观察者;
keyPath:要移除的属性名称;
注意:一定要在合适的机会移除观察者,否则会引发泄露,我将会在下一篇中列举出。

(2)触发接收方法的几种情况

  1. 直接调用setter方法,或者通过属性的点语法间接调用;
  2. 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
  3. 通过mutableArrayValueForKey:K方法获取到数组代理对象,并使用代理对象进行操作;
    注意:直接给成员赋值是不会触发的

(3)使用案例

案例一:普通操作

#import 

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSNumber *PID;  //人ID
@property (nonatomic, copy) NSString *name;  //名字
@property (nonatomic, copy) NSString *address;  //地址

@end

NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p = [[Person alloc] init];
    self.person = p;
    
    [p addObserver:self forKeyPath:@"PID" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    
}

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //点击屏幕,设置person的PID属性值
    static NSInteger count = 1;
    count++;
    self.person.PID = @(count);
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"PID"];
    self.person = nil;
    NSLog(@"%@ dealloc", [self class]);
}


@end

点击屏幕输出结果:

2019-07-02 21:58:12.528122+0800 KVODemo[8046:234497] {
    kind = 1;
    new = 2;
    old = "";
}

案例二:观察Person对象中的Info对象的name属性

#import 

NS_ASSUME_NONNULL_BEGIN

@interface Info : NSObject

@property (nonatomic, copy) NSString *name;  //名字
@property (nonatomic, copy) NSString *address;  //地址

@end

NS_ASSUME_NONNULL_END
#import 
#import "Info.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSNumber *PID;  //人ID
@property (nonatomic, strong) Info *info; //个人信息

@end

NS_ASSUME_NONNULL_END

#import "Person.h"

@implementation Person

- (instancetype)init
{
    self = [super init];
    
    if ( self ) {
        _info = [Info new];
    }
    
    return self;
}

@end

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p = [[Person alloc] init];
    self.person = p;
    //观察person的info的name属性
    [self.person addObserver:self forKeyPath:@"info.name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    
}

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //点击屏幕,设置person的info的name属性值
    self.person.info.name = @"hui";
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"info.name"];
    self.person = nil;
    NSLog(@"%@ dealloc", [self class]);
}


@end

点击屏幕输出结果:

2019-07-02 22:40:45.128484+0800 KVODemo[8667:250652] {
    kind = 1;
    new = hui;
    old = "";
}

案例三:观察Person对象中的Info对象多个属性
代码就不全部贴了,只贴重要代码;
主要的是在被观察者的对象中重写

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key;

根据自己的需求,添加观察的哪些属性。如我想观察info对象中的name和address的属性。
那么代码如下:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ( [key isEqualToString:@"info"] ) {
        
        keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"_info.name", @"_info.address"]];
        
    }
    
    return keyPaths;
}

观察的keyPaths为info。

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

当info对象的name或者address属性变化时候,将会自动调用接收方法。

案例四:手动触发
注册后,KVO默认会自动通知观察者。其实我们可以手动触发,在满足某些条件我们在触发调用接收方法。

如果你想取消自动通知,主动在被观察者类中实现下面方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
返回值:
  YES:主动触发;
  NO:自动触发;

针对非自动通知的属性,可以分别在变化之前和之后手动调用如下方法(will在前,did在后)来手动通知观察者:

-(will/did)ChangeValueForKey:
-(will/did)ChangeValueForKey:withSetMutation:usingObjects:
-(will/did)Change:valuesAtIndexes:forKey:

实际上自动通知也是框架通过调用这些方法实现的。
直接用案例一的代码,不全部列出来了。只列举重要的部分。
观察Person对象的name属性,不让其主动触发。在Person类中加入

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ( [key isEqualToString:@"name"] ) {
        return NO;
    }
    return YES;
}

触摸屏幕时主动触发接收方法。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //点击屏幕,设置person的info的name属性值
    [self willChangeValueForKey:@"name"];
    self.person.name = @"hui";
    [self didChangeValueForKey:@"name"];
}

over!

你可能感兴趣的:(iOS-KVO(一) 基本操作)