KVO的定義
???鍵值觀察,對象采用的一種非正式協(xié)議,用于將其他對象的指定屬性的更改通知給對象??梢杂^察到任何對象屬性,包括簡單屬性,一對一關(guān)系和一對多關(guān)系。一對多關(guān)系的觀察者被告知所做更改的類型,以及更改涉及哪些對象。NSObject
提供了NSKeyValueObserving協(xié)議的實(shí)現(xiàn),該協(xié)議為所有對象提供了自動觀察功能。開發(fā)者可以通過禁用自動觀察者通知,并使用此協(xié)議中的方法實(shí)施手動通知來進(jìn)一步優(yōu)化通知。
常用方法和參數(shù)
注冊觀察
注冊觀察者
-addObserver:forKeyPath:options:context:
移除觀察者。
-removeObserver:forKeyPath:
在給定上下文的情況下,移除觀察者。
-removeObserver:forKeyPath:context:
變更通知
鍵值發(fā)生改變時,通知觀察對象。
-observeValueForKeyPath:ofObject:change:context:
通知觀察者變更
通知觀察到的對象給定屬性的值即將更改。
-willChangeValueForKey:
通知觀察到的對象給定屬性的值已更改。
-didChangeValueForKey:
通知觀察對象對于指定的有序多對關(guān)系,將在給定的索引處執(zhí)行指定的更改。
-willChange:valuesAtIndexes:forKey:
通知觀察對象指定的多對多關(guān)系在索引上發(fā)生了指定的更改。
-didChange:valuesAtIndexes:forKey:
通知觀察對象即將對指定的無序多對關(guān)系進(jìn)行指定的更改。
-willChangeValueForKey:withSetMutation:usingObjects:
通知觀察對象對指定的無序?qū)Χ嚓P(guān)系進(jìn)行了指定的更改。
-didChangeValueForKey:withSetMutation:usingObjects:
觀察定制
是否允許自動鍵值觀察。
+automaticallyNotifiesObserversForKey:
示例:name屬性不允許自動觀察
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
// 手動觀察name屬性
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
為屬性的值返回一組鍵路徑,觀察多個鍵值時使用,任一鍵值有更新,都會被通知到。
+keyPathsForValuesAffectingValueForKey:
示例:觀察Person的dog屬性中name、age的變化
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) Dog *dog;
@property (nonatomic, strong) NSMutableArray *arr;
@end
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
_dog = [[Dog alloc] init];
_arr = [NSMutableArray array];
}
return self;
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
// 此處一定要帶下劃線
NSArray *arr = @[@"_dog.name", @"_dog.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
}
return keyPaths;
}
@end
@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
#import "KVOViewController.h"
#import "Person.h"
@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation KVOViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
Person *person = [Person new];
_person = person;
// 觀察person內(nèi)dog屬性的name和age變化
// [_person addObserver:self forKeyPath:@"dog.name" options:(NSKeyValueObservingOptionNew) context:nil];
// [_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
[_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
_person.dog.name = name;
_person.dog.age = i++;
}
@end
如果給定數(shù)組中指定的任何屬性發(fā)生更改,則將觀察對象配置為發(fā)布給定屬性的更改通知。不推薦使用
+setKeys:triggerChangeNotificationsForDependentKey:
返回一個指針,該指針標(biāo)識有關(guān)向觀察對象注冊的所有觀察者的信息。
observationInfo
參數(shù)
可以觀察到的變化類型。
NSKeyValueChange
NSKeyValueChangeSetting // 觀察set方法
NSKeyValueChangeInsertion // 觀察集合 insert 元素操作
NSKeyValueChangeRemoval // 觀察集合 remove 元素操作
NSKeyValueChangeReplacement // 觀察集合 replace 元素操作
可以在變更字典中返回的值。
NSKeyValueObservingOptions
NSKeyValueObservingOptionOld // 表示在change字典中包含了改變前的值。
NSKeyValueObservingOptionNew // 表示在change字典中包含新的值。
NSKeyValueObservingOptionInitial // 在注冊觀察者的方法return的時候就會發(fā)出一次通知。
NSKeyValueObservingOptionPrior // 在值發(fā)生改變前、后各發(fā)出一次通知,每次change都會有兩個通知。
可以顯示在更改字典中的鍵。
NSKeyValueChangeKey
NSKeyValueChangeKindKey // 指明了變更的類型,值為“NSKeyValueChange”枚舉中的某一個,類型為NSNumber
NSKeyValueChangeNewKey // 被監(jiān)聽屬性改變后新值的key,當(dāng)監(jiān)聽屬性為一個集合對象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時,該值返回的是一個數(shù)組,包含插入,替換后的新值(刪除操作不會返回新值)
NSKeyValueChangeOldKey // 被監(jiān)聽屬性改變前舊值的key,當(dāng)監(jiān)聽屬性為一個集合對象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時,該值返回的是一個數(shù)組,包含刪除,替換前的舊值(插入操作不會返回舊值)
NSKeyValueChangeIndexesKey // 如果NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,這個鍵的值是一個NSIndexSet對象,包含了增加,移除或者替換對象的index。
NSKeyValueChangeNotificationIsPriorKey // 如果注冊監(jiān)聽者是options中指明了NSKeyValueObservingOptionPrior,change字典中就會帶有這個key,值為NSNumber類型的YES.
可以對無序集合進(jìn)行的突變類型。
NSKeyValueSetMutationKind
NSKeyValueUnionSetMutation // 已將指定集中的觀察者添加到觀察對象。
NSKeyValueMinusSetMutation // 正在從觀察對象中除去指定集合中的觀察者
NSKeyValueIntersectSetMutation // 正在從觀察對象中移除不在指定集合中的觀察者。
NSKeyValueSetSetMutation // 一組觀察者正在替換觀察對象中的現(xiàn)有對象。
KVO的內(nèi)部實(shí)現(xiàn)
???當(dāng)我們對Person對象的name屬性進(jìn)行觀察的時候,實(shí)際上會在-addObserver:forKeyPath:options:context:
內(nèi)部動態(tài)創(chuàng)建一個Person的子類 NSKVONotifying_Person,動態(tài)修改觀察對象的類型,觀察子類,然后重寫子類的set方法來實(shí)現(xiàn)。
可以發(fā)現(xiàn),在addObserver:forKeyPath:options:context:
執(zhí)行前,person的isa指針指向Person,addObserver:forKeyPath:options:context
執(zhí)行后,person的isa指針指向了NSKVONotifying_Person,說明在addObserver:forKeyPath:options:context
方法內(nèi)部發(fā)生了指針混寫。
自定義KVO的監(jiān)聽
新建一個NSObject的分類NSObject+JRKVO,定義方法:jr_addObserver: forKeyPath: options: context:
,在方法實(shí)現(xiàn)中如下代碼:
#import "NSObject+JRKVO.h"
#import <objc/message.h>
static const char *JRKVO_observer = "JRKVO_observer";
static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";
@implementation NSObject (JRKVO)
- (void)jr_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context {
// 創(chuàng)建、注冊子類
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
Class newClass = objc_getClass(newClassName.UTF8String);
if (!newClass) {
// 創(chuàng)建類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注冊類
objc_registerClassPair(newClass);
}
// setter方法首字母大寫
NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
// 動態(tài)修改監(jiān)聽的類型,監(jiān)聽子類
object_setClass(self, newClass);
// 子類添加setter方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(newClass, setSEL, (IMP)setMethod, types);
// 保存Observer
objc_setAssociatedObject(self, JRKVO_observer, observer, OBJC_ASSOCIATION_ASSIGN);
// 保存setter、getter方法
objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
void setMethod(id self, SEL _cmd, id newValue) {
// 獲取setter、getter方法
NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);
// 保存子類類型
Class class = [self class];
// isa指向原類
object_setClass(self, class_getSuperclass(class));
// 調(diào)用原類getter方法,獲取舊值
id oldValue = objc_msgSend(self, NSSelectorFromString(getName));
// 調(diào)用原類setter方法
objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);
id observer = objc_getAssociatedObject(self, JRKVO_observer);
NSMutableDictionary *change = [@{} mutableCopy];
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
// 通知觀察者
objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), getName, self, change, nil);
// isa重新指向子類
object_setClass(self, class);
}
@end
調(diào)用jr_addObserver: forKeyPath: options: context:
方法,觀察person的name屬性
[_person jr_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
改變person的name屬性的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
// _person.name = name;
objc_msgSend(_person, @selector(setName:), name);
}
set方法已經(jīng)執(zhí)行,并且觀察者回調(diào)方法中已經(jīng)輸出name新值、舊值。
自定義KVO的監(jiān)聽 帶block回調(diào)
在上面創(chuàng)建的NSObject分類NSObject+JRKVO中,定義方法:jr_addObserverForKeyPath: options: callbak:
,在方法實(shí)現(xiàn)中如下代碼:
#import "NSObject+JRKVO.h"
#import <objc/message.h>
static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";
static const char *JRKVO_callback = "JRKVO_callback";
@implementation NSObject (JRKVO)
- (void)jr_addObserverForKeyPath:(NSString *_Nonnull)keyPath
options:(NSKeyValueObservingOptions)options
callbak:(JRKVOCallbackBlock _Nonnull )block {
// 創(chuàng)建、注冊子類
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
Class newClass = objc_getClass(newClassName.UTF8String);
if (!newClass) {
// 創(chuàng)建類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注冊類
objc_registerClassPair(newClass);
}
// setter方法首字母大寫
NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
// 動態(tài)修改監(jiān)聽的類型,監(jiān)聽子類
object_setClass(self, newClass);
// 子類添加setter方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(newClass, setSEL, (IMP)setMethod, types);
// 保存block
objc_setAssociatedObject(self, JRKVO_callback, block, OBJC_ASSOCIATION_COPY);
// 保存setter、getter方法
objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
void setMethod(id self, SEL _cmd, id newValue) {
// 獲取setter、getter方法
NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);
// 保存子類類型
Class class = [self class];
// isa指向原類
object_setClass(self, class_getSuperclass(class));
// 調(diào)用原類getter方法,獲取舊值
id oldValue = objc_msgSend(self, NSSelectorFromString(getName));
// 調(diào)用原類setter方法
objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);
NSMutableDictionary *change = [@{} mutableCopy];
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
// 回調(diào)setter方法值的改變
JRKVOCallbackBlock callback = objc_getAssociatedObject(self, JRKVO_callback);
if (callback) {
callback(change);
}
// isa重新指向子類
object_setClass(self, class);
}
@end
調(diào)用jr_addObserverForKeyPath: options: callbak:
方法,觀察person的name屬性變化。
觀察多個鍵值的變化
通keyPathsForValuesAffectingValueForKey:
實(shí)現(xiàn)多個鍵值觀察
示例:觀察Person的dog屬性中name、age的變化
Person 類中實(shí)現(xiàn)如下方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
// 此處一定要帶下劃線
NSArray *arr = @[@"_dog.name", @"_dog.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
}
return keyPaths;
}
VC中觀察Person的dog屬性
[_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"dog 改變了%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
_person.dog.name = name;
_person.dog.age = i++;
}
點(diǎn)擊屏幕,改變dog對象name、age的值,打印結(jié)果通過keyPathsForValuesAffectingValueForKey:
把多個keyPath放在一個集合set中,統(tǒng)一觀察,dog的任何一個屬性發(fā)生變化,都會通知到。比起單個keyPath觀察,確實(shí)方便了很多。
數(shù)組的觀察
通過KVC中的mutableArrayValueForKey:
實(shí)現(xiàn)數(shù)組的觀察,數(shù)組中元素改變(增、刪、改)都可以觀察到。
示例代碼:
#import "KVOViewController.h"
#import "Person.h"
#import "NSObject+JRKVO.h"
#import <objc/message.h>
@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation KVOViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
Person *person = [Person new];
_person = person;
// 觀察 person的arr屬性的變化, 數(shù)組元素的增刪改
[_person addObserver:self forKeyPath:@"arr" options:(NSKeyValueObservingOptionNew) context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"Person中的arr改變了%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
// 通過KVC獲取到數(shù)組,
NSMutableArray *arr = [_person mutableArrayValueForKey:@"arr"];
// add 對應(yīng) NSKeyValueChange 為 NSKeyValueChangeInsertion, 即:kind 為 2
[arr addObject:name];
// replace 對應(yīng) NSKeyValueChange 為 NSKeyValueChangeReplacement, 即:kind 為 4
[arr replaceObjectAtIndex:0 withObject:@"1111"];
// remove 對應(yīng) NSKeyValueChange 為 NSKeyValueChangeRemoval, 即:kind 為 3
[arr removeObjectAtIndex:0];
}
@end
點(diǎn)擊屏幕,操作數(shù)組元素的增、刪、改,打印結(jié)果:
執(zhí)行完
mutableArrayValueForKey:
后,觀察arr的類型已經(jīng)變成NSKeyValueMutableArray類型,其實(shí)mutableArrayValueForKey:
內(nèi)部動態(tài)創(chuàng)建了NSMutableArray的子類NSKeyValueMutableArray,然后指針混寫指向子類,并返回子類,后續(xù)觀察arr實(shí)際上是觀察子類。
這里的kind = 2,kind = 3,kind = 4, 分別對應(yīng)NSKeyValueChange
的 NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
。也就是說KVO不僅僅能觀察對象的set方法,當(dāng)對象為集合類型時,也能觀察集合元素的增、刪、改,只不過觀察集合對象要通過KVC的mutableArrayValueForKey:
方法來獲取集合。類似通過person.arr
的方式獲取數(shù)組,操作數(shù)組元素的增、刪、改是觀察不到的。