KVO的使用

2018-04-13  本文已影响34人  云天涯丶

一、介绍

KVO(NSKeyValueObserving):是一种非正式协议,当被观察的对象(比如A)的属性(比如name)改变时,观察者(比如VC)就会得到通知,然后做出相应处理。

NSObject提供了NSKeyValueObserving协议的实现,所以几乎所有的类都可以使用KVO。

KVO 的实现依赖于 Objective-C 强大的 Runtime, Apple 的文档对 KVO 机制的实现说的很简单:KVO是用isa-swizzling技术实现的,当观察者注册了对象的属性时,被观察对象的isa指针被修改,指向中间类而不是真实类...

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

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

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

二、api

@interface NSObject(NSKeyValueObserving)
// 观察属性的通知方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

@end

@interface NSObject(NSKeyValueObserverRegistration)
// 注册、移除 观察者 
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSArray<ObjectType>(NSKeyValueObserverRegistration)
// 注册、移除 观察者  NSArray
- (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath;

// 注册、移除 观察者  NSArray
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSOrderedSet<ObjectType>(NSKeyValueObserverRegistration)
// 注册、移除 观察者  NSOrderedSet
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSSet<ObjectType>(NSKeyValueObserverRegistration)
// 注册、移除 观察者  NSSet
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSObject(NSKeyValueObserverNotification)
// 手动触发KVO时重要方法
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;

@end

@interface NSObject(NSKeyValueObservingCustomization)

// KVO 依赖键
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

// 返回yes:自动触发KVO;若要手动触发KVO,返回no
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

@end

三、例子
1、基本用法

Person类里有个name属性

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

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [Person new];
    [self.p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    self.p.name = @"a";
    self.p.name = @"b";
    self.p.name = @"c";
}

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

NSKeyValueObservingOptions:有四个值,分别是
NSKeyValueObservingOptionNew = 0x01, // 新值
NSKeyValueObservingOptionOld = 0x02, // 旧值
NSKeyValueObservingOptionInitial = 0x04,// 注册通知也会触发
NSKeyValueObservingOptionPrior = 0x08 // 值修改前后触发

2、手动KVO
Person.m

@synthesize name = _name;

- (void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

- (NSString *)name{
    return _name;
}

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

手动触发的话要重写automaticallyNotifiesObserversForKey方法,return NO,不然观察者的通知方法走两遍

3、KVO 依赖键
有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。

KVO 依赖键有两种方法:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
+ (NSSet<NSString *> *)keyPathsForValuesAffecting<Key>;

下面分别用这两个方法实现:

// Student有两个属性:age和school
@interface Student : NSObject

@property (nonatomic,assign) NSInteger age;
@property (nonatomic,copy) NSString *school;

@end


// Person类
@interface Person : NSObject

@property (nonatomic,copy) NSString *info;
@property (nonatomic,strong) Student *stu;

@end

@implementation Person

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

- (NSString *)info{
    return [NSString stringWithFormat:@"小敏%ld岁了,在%@上学",_stu.age,_stu.school];
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingInfo{
    NSSet *keyPaths = [NSSet setWithObjects:@"stu.age",@"stu.school", nil];
    return keyPaths;
}

//+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
//
//    if ([key isEqualToString:@"info"]) {
//        return [NSSet setWithObjects:@"stu.age",@"stu.school", nil];
//    }
//
//    return [super keyPathsForValuesAffectingValueForKey:key];
//}

@end

// 调用
@interface ViewController ()

@property (nonatomic,strong) Person *p;

@end

@implementation ViewController

- (void)dealloc{
    [self.p removeObserver:self forKeyPath:@"info" context:nil];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [Person new];
    
    [self.p addObserver:self forKeyPath:@"info" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"cc"];
    self.p.stu.age = 18;
    self.p.stu.school = @"北大";
    
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // descriptionWithLocale 转码处理
    NSLog(@"%@ %@ %@",keyPath,object,[change descriptionWithLocale:nil]);
}

结果:

info <Person: 0x60c000036860> {
    kind = 1;
    new = "小敏18岁了,在(null)上学";
    old = "小敏0岁了,在(null)上学";
}
2018-04-13 14:19:49.729454+0800 test[5259:597332] info <Person: 0x60c000036860> {
    kind = 1;
    new = "小敏18岁了,在北大上学";
    old = "小敏18岁了,在(null)上学";
}

上一篇下一篇

猜你喜欢

热点阅读