KVO 键值观察

KVO - 键值观察

KVOKey-Value Observing)是一种允许将其他对象的特定属性的变更通知到观察对象的机制。想要理解KVO,最好先了解下KVC(key-value coding)

基础介绍

注册、移除注册、接收通知

NSObject(NSKeyValueObserverRegistration)

一般而言,通过下面的方法进行添加注册、移除注册和接收通知。

添加注册
该方法不会对观察对象、被观察对象和context保持强引用;
- (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;
- (void)removeObserver:(NSObject *)observer 
			  forKeyPath:(NSString *)keyPath;

应该尽可能使用 -removeObserver:forKeyPath:context: 
而不是 -removeObserver:forKeyPath: ,因为它可以让您更准确地指定您的意图。

通知
- (void)observeValueForKeyPath:(nullable NSString *)keyPath 
						  ofObject:(nullable id)object 
						    change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change 
						    context:(nullable void *)context;

注意NSArrayNSOrderedSetNSSet都是不可以作为被观察对象的。调用以上方法将导致程序抛出异常。

NSArray(NSKeyValueObserverRegistration)

对于NSArray虽然不能直接作为被观察对象,以观察者的身份注册或注销与数组中每个索引元素相关的键路径上的值。这种方式可能比元素对对象反复调用NSObject(NSKeyValueObserverRegistration)方法快得多。

添加注册
- (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;
- (void)removeObserver:(NSObject *)observer 
  fromObjectsAtIndexes:(NSIndexSet *)indexes 
  			  forKeyPath:(NSString *)keyPath;

NSKeyValueObservingOptions

通过添加注册的选项NSKeyValueObservingOptions来订制通知变更中的内容。

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {

    NSKeyValueObservingOptionNew = 0x01, // 新值
    NSKeyValueObservingOptionOld = 0x02, // 旧值
    NSKeyValueObservingOptionInitial, // 注册代码返回前发送通知
    /*
    变更前:包含NSKeyValueChangeNotificationIsPriorKey:@YES,不包含NSKeyValueObservingOptionNew。
    	由set<Key>中值变更前-willChange…触发。
    更改后:发送的通知中的更改字典包含与未指定此选项时相同。
    	
	但由NSOrderedSets表示的有序唯一对多关系除外。对于这些,对于NSKeyValueChangeInsertion和NSKeyValueChangeReplacement变化,
	will-change通知的更改字典包含一个NSKeyValueChangeIndexesKey(在替换的情况下且指定了NSKeyValueObservingOptionOld时,包含NSKeyValueChangeOldKey),它给出了可能被操作更改的索引(和对象)。更改之后的第二个通知包含了报告实际更改的内容的条目。对于NSKeyValueChangeRemoval更改,通过索引可以精确地删除。
    */
    NSKeyValueObservingOptionPrior // 属性在更改之前和之后分别发送通知
 
};

NSKeyValueChange

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
	/*
	表示被观察对象收到-setValue:forKey:消息,或者其他符合KVC的set方法被调用。再或者 -willChangeValueForKey:/-didChangeValueForKey: 方法对被调用。
	*/
    NSKeyValueChangeSetting = 1, // 设置
    
    NSKeyValueChangeInsertion = 2, // 插入
    NSKeyValueChangeRemoval = 3, // 移除
    NSKeyValueChangeReplacement = 4, // 替换
  
    对于有序集合
    -mutableArrayValueForKey: 
    -mutableOrderedSetValueForKey: 
    -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:
    无序集合
    -mutableSetValueForKey:
	-willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: 
};

NSKeyValueSetMutationKind

集合属性的事件类型

typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
    NSKeyValueUnionSetMutation = 1, // 对应 NSKeyValueChangeInsertion:[NSMutableSet unionSet:]
    NSKeyValueMinusSetMutation = 2, // 对应 NSKeyValueChangeRemoval:[NSMutableSet minusSet:]
    NSKeyValueIntersectSetMutation = 3, // 对应 NSKeyValueChangeRemoval:[NSMutableSet intersectSet:]
    NSKeyValueSetSetMutation = 4 // 对应 NSKeyValueChangeReplacement:[NSMutableSet setSet:]
};

NSKeyValueChangeKey

change 字典中的key值

FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey; //@"kind"
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey; //@"new"
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey; //@"old"
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey; //@"indexes"
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey //@"notificationIsPrior"

NSObject(NSKeyValueObserverNotification)

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

自动更改通知 & 手动更改通知

你可以通过实现类方法automaticallyNotifiesObserversForKey:来控制你子类属性是否可以自动通知。

自动更改通知

自动键值观察是使用一种称为isa-swizzling的技术实现的。
isa指针,顾名思义,指向维护调度表的对象的类。这个分派表实际上包含指向类实现的方法和其他数据的指针。
当观察者被一个对象的属性注册时,被观察对象的isa指针就被修改为指向一个中间类,而不是真正的类。因此,isa指针的值不一定反映实例的实际类。因此永远不要依赖isa指针来确定类成员。相反,您应该使用类方法来确定对象实例的类。

手动更改通知

可以通过+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey返回NO去除自动通知。

为了实现手动的观察者通知,在值改变之前调用willChangeValueForKey:,在值改变之后调用didChangeValueForKey:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
如果单个操作导致多个键发生更改,则必须嵌套更改通知
- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
有序且对多关系

在有序且对多关系的情况下,不仅必须指定更改的键,还必须指定更改的类型和涉及的对象的索引。变化的类型是NSKeyValueChange,指定NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement。受影响对象的索引作为NSIndexSet对象传递。

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

关联属性

在许多情况下,一个属性的值依赖于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生更改,则派生属性的值也应标记为更改。所以应确保根据关联属性的关系基数为这些依赖属性发布键值观察的通知。

一对一关系

要为一对一关系的属性值变更自动触发通知,应该覆盖keyPathsForValuesAffectingValueForKey:或实现一个合适的方法,遵循它定义的用于注册依赖键的模式。

例如,一个人的全名取决于他的姓和名。返回全名的方法可以这样写:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

在观察fullName情况下,所以当firstNamelastName属性更改时,必须通知观察者fullName属性值变更了。

  • 解决方案一:

覆盖keyPathsForValuesAffectingValueForKey:指定一个人的fullName属性依赖于lastNamefirstName属性。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  • 解决方案二:
    可以通过实现遵循命名约定keyPathsForValuesAffecting<Key>的类方法来实现相同的结果。其中<Key>是依赖于其他值的属性的名称(首字母大写)。
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

当使用类别向现有类添加计算属性时,不能覆盖keyPathsForValuesAffectingValueForKey:方法。在这种情况下,实现一个匹配的keyPathsForValuesAffecting<Key>类方法是最好的选择。

注意:你不能通过实现keyPathsForValuesAffectingValueForKey:来建立对多关系的依赖。相反,您必须观察to-many集合中每个对象的适当属性,并通过自己更新依赖键来响应它们值的更改。

对多关系

keyPathsForValuesAffectingValueForKey:方法不支持包含对多关系的key-paths
比如有一个Department对象,该对象与Employee之间有一个对多关系(employees),而Employee具有一个salary属性。您可能希望Department对象具有一个totalSalary属性,该属性依赖于关系中所有employee的工资。此时不可以通过keyPathsForValuesAffectingTotalSalary方法返回employees.salary作为key来实现。

这种情况有两个解决方案:

一、可以使用KVO将父节点(在本例中为Department)注册为所有子节点(在本例中为Employees)相关属性的观察者。在子对象被添加到关系中或从关系中删除时候,必须添加和删除父对象作为观察者。在observeValueForKeyPath:ofObject:change:context:方法中,你更新依赖值来响应变化,如下面的代码片段所示:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

二、如果您正在使用Core Data,您可以将父对象注册到应用程序的通知中心,作为其托管对象上下文的观察者。父对象应以类似于key-value观察的方式响应子对象发布的相关更改通知。