前言
KVO(Key-Value Obsering)鍵值觀察袁滥。KVO是一種機(jī)制录择,該機(jī)制允許將需要被觀察的對象的指定屬性的更改通知給發(fā)送給觀察的對象脓钾。直接來說就是對某個對象的屬性的觀察監(jiān)聽宾濒,如果被觀察的屬性有發(fā)生了變化會以通知的形式發(fā)送給觀察的對象帕翻。KVO是基于KVC的基礎(chǔ)上的宠进,本人之前也寫了一篇介紹KVC的文章晕拆,具體可以看iOS的Key-Value Coding
KVO的主要好處是,您不必為每次更改屬性而寫一些其他代碼即可發(fā)送通知材蹬。但是與NSNotificationCenter
通知不同实幕,NSNotificationCenter
沒有中間對象
為所有觀察者提供更改通知。而是在進(jìn)行更改時將通知直接發(fā)送到觀察對象堤器。并且NSObject
提供了鍵值觀察的基本實(shí)現(xiàn)昆庇,只要是繼承NSObject
就可以實(shí)現(xiàn)。
1.KVO的使用
通過實(shí)現(xiàn)以下三個步驟可以使對象接收KVO兼容屬性的鍵值觀察通知:
1.使用方法addObserver:forKeyPath:options:context:
將觀察者注冊到觀察對象闸溃。
2.在觀察者內(nèi)部實(shí)現(xiàn)方法observeValueForKeyPath:ofObject:change:context:
接收更改的通知整吆。
3.當(dāng)不再接收消息時拱撵,可以使用方法removeObserver:forKeyPath:
注銷觀察者,至少在銷毀觀察者之前調(diào)用這個方法表蝙。
2.KVO的注冊
使用addObserver:forKeyPath:options:context:
方法為需要觀察的屬性注冊一個觀察者裕膀。其中分別對options和context值進(jìn)行說明。
2.1 Option
options是一個NSKeyValueObservingOptions枚舉類型勇哗。在使用的時候可以單獨(dú)使用也可以用|
符號多個連接使用昼扛。
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:指明接受通知方法參數(shù)中的
chang
字典中包含改變后的新值,默認(rèn)情況下也是只接收新值欲诺。 -
NSKeyValueObservingOptionOld:指明接受通知方法參數(shù)中的
change
字典中包含改變前的舊值抄谐。 -
NSKeyValueObservingOptionInitial:當(dāng)指定了這個選項(xiàng)時,在
addObserver:forKeyPath:options:context:
消息被發(fā)出去后扰法,甚至不用等待這個消息返回蛹含,觀察者對象會馬上收到一個通知。這種通知只會發(fā)送一次塞颁,你可以利用這種“一次性”
的通知來確定要觀察屬性的初始值浦箱。 -
NSKeyValueObservingOptionPrior:當(dāng)包含這個參數(shù)的時候,在被觀察的屬性的值改變前和改變后祠锣,系統(tǒng)各會給觀察者發(fā)送一個改變的通知酷窥;在屬性的值改變之前發(fā)送的改變的通知中,參數(shù)會包含
NSKeyValueChangeNotificationIsPriorKey
并且值為@YES伴网,但不會包含NSKeyValueChangeNewKey
和它對應(yīng)的值蓬推。
2.2 Context
context指針可以是任意數(shù)據(jù),這些數(shù)據(jù)將在相應(yīng)的更改通知中傳遞回觀察者澡腾,也可以將context的值設(shè)置為NULL
再通過依賴keyPath
鍵值路徑字符串來確定更改通知的來源沸伏。但是這種方式會引發(fā)出問題,比如如果父類和子類都監(jiān)聽了相同的KeyPath鍵值路徑的話动分,這時就很難區(qū)分出來了毅糟。可能也有人會說澜公,可以根據(jù)observeValueForKeyPath:ofObject:change:context:
方法的object來做判斷姆另,但是如果這樣的就有多層的嵌套,在沒有寫核心代碼的時候就有這樣的嵌套就顯得代碼很不優(yōu)雅.
注意:為什么是NULL不是nil呢玛瘸?因?yàn)镺C是C的超集蜕青,并且Context的參數(shù)指針類型的,
所以是NULL糊渊。什么時候可以是nil呢右核?一般是實(shí)例的時候可以為nil,類的時候Nil渺绒,指針的時候?yàn)镹ULL.
為了避免出現(xiàn)這種問題可以使用命名靜態(tài)變量地址的形式來設(shè)置context的值贺喝,可以為整個類選擇一個上下文菱鸥,然后依靠通知消息中的keyPath鍵路徑字符串來確定更改的內(nèi)容。另外躏鱼,還可以為每個觀察屬性的keyPath創(chuàng)建一個不同的context
氮采,從而完全不需要進(jìn)行字符串比較,從而可以更有效地進(jìn)行通知解析例如:
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *PersonFullNameContext = &PersonFullNameContext;
static void *PersonDataArrayContext = &PersonDataArrayContext;
在observeValueForKeyPath:ofObject:change:context:
方法中大概的實(shí)現(xiàn)和結(jié)果
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context{
if(context == PersonNameContext){
NSLog(@"處理name的代碼:%@",change);
}else if(context == PersonNickNameContext){
NSLog(@"處理nickName的代碼:%@",change);
}else{
[super observeValueForKeyPath:keyPath
ofObject:object change:change context:context];
}
}
3.KVO的移除
在注冊使用完KVO了染苛,就需要對KVO移除鹊漠,實(shí)現(xiàn)調(diào)用removeObserver:forKeyPath:context:
方法。因?yàn)橛^擦對象不會自動移除已經(jīng)注冊的KVO茶行,所以注冊和刪除KVO這兩個是需要成對出現(xiàn)的躯概,一般都是在init
或者viewDidLoad
方法中進(jìn)行注冊,在delloc
方法中進(jìn)行刪除畔师,如果沒有移除會引發(fā)野指針錯誤娶靡。
4.手動更改通知
一般情況下,我們使用KVO的時候都是調(diào)用的系統(tǒng)的自動更改通知的看锉。但是姿锭,KVO也可以是手動設(shè)置的,需要觀察的對象里實(shí)現(xiàn)類方法automaticallyNotifiesObserversForKey
默認(rèn)是返回YES的伯铣。如果設(shè)置返回NO呻此,并且在觀察屬性值之前調(diào)用willChangeValueForKey:
和觀察值之后調(diào)用didChangeValueForKey:
就改為手動的更改通知了。下面創(chuàng)建一個Person對象來簡單驗(yàn)證一下懂傀。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property(nonatomic,copy) NSString *fullName;
@property(nonatomic,copy) NSString *name;
@property(nonatomic,copy) NSString *nickName;
@property (nonatomic, strong) NSMutableArray *dateArray;
@end
NS_ASSUME_NONNULL_END
@implementation Person
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
@end
這是實(shí)現(xiàn)的部分代碼:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
self.person = [[Person alloc] init];
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"name"];
self.person.name = @"jason";
[self.person didChangeValueForKey:@"name"];
self.person.nickName = @"煙火";
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if(context == PersonNameContext){
NSLog(@"處理name的代碼:%@",change);
}else if(context == PersonNickNameContext){
NSLog(@"處理nickName的代碼:%@",change);
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
打印的結(jié)果:
2020-04-18 16:29:57.789887+0800 KVODemo[4950:209403] 處理name的代碼:{
kind = 1;
new = jason;
}
這時候會發(fā)現(xiàn)趾诗,如果改為手動更改通知的時候,那么例子中nickName這個屬性的自動更改通知就不會實(shí)現(xiàn)了蹬蚁。
如果一個操作造成了多個key的值的改變,則willChangeValueForKey:和didChangeValueForKey:必須嵌套著調(diào)用郑兴。官方文檔的例子:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
5.一對一的關(guān)系
例如:在Person中的fullName是依賴于name和nickName來設(shè)置值犀斋,在Person中獲取fullName的方法:
- (NSString *)fullName{
return [NSString stringWithFormat:@"%@--%@",self.name,self.nickName];
}
這時候?yàn)榱擞^察fullName的值變化在Person中可以實(shí)現(xiàn)類方法keyPathsForValuesAffectingValueForKey:
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"name", @"nickName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
實(shí)現(xiàn)的部分代碼:
//監(jiān)聽的
[self.person addObserver:self forKeyPath:@"fullName"
options:NSKeyValueObservingOptionNew context:PersonFullNameContext];
//對name和nickName的修改的
self.person.name = @"jason";
self.person.nickName = @"煙火";
//觀擦的回調(diào)
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context{
if(context == PersonFullNameContext){
NSLog(@"fullName:%@",change[@"new"]);
}
else{
[super observeValueForKeyPath:keyPath
ofObject:object change:change context:context];
}
}
//打印的結(jié)果
2020-04-18 17:13:55.894622+0800 KVODemo[5486:237940] fullName:jason--煙火
6.多對多的關(guān)系
例如:在對Person中可變數(shù)組dateArray屬性進(jìn)行觀察,如果按照上面的方式來注冊情连,然后對數(shù)組添加數(shù)據(jù)叽粹,再監(jiān)聽context的值做操作,部分代碼:
//注冊數(shù)組屬性
[self.person addObserver:self forKeyPath:@"dateArray"
options:NSKeyValueObservingOptionNew context:PersonDataArrayContext];
//數(shù)組添加數(shù)據(jù)
[self.person.dateArray addObject:@"1"];
//監(jiān)聽
if(context == PersonDataArrayContext){
NSLog(@"dataArray:%@",change[@"new"]);
}
但是這時候發(fā)現(xiàn)却舀,控制臺里什么東西都沒有打印出來虫几。這是為什么呢?因?yàn)镵VO對屬性的setter方法進(jìn)行監(jiān)聽的挽拔,可變數(shù)組的addObject方法沒有setter方法辆脸,所以就監(jiān)聽不了。但是是不是說這樣就監(jiān)聽不了數(shù)組了呢螃诅?并不是的啡氢,因?yàn)镵VO是建立在KVC的基礎(chǔ)上的状囱,主要改為
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
就可以監(jiān)聽到了。
7.原理
7.1 isa_swizzling
根據(jù)蘋果的官方文檔可以知道倘是,KVO的實(shí)現(xiàn)原理是通過對象的isa
交換即isa-swizzling
亭枷。isa
指針就是指向?qū)ο蟮念悾趯ο鬄閷傩宰訩VO的時候搀崭,將修改觀察對象的isa指針
叨粘,指向中間類而不是真實(shí)類,所以isa指針的值不一定反映實(shí)例的實(shí)際類瘤睹,不能依靠isa指針來確定類成員升敲,應(yīng)該使用class
類方法來確定對象實(shí)例的類。
為了驗(yàn)證這個說法默蚌,還是用上一篇文章介紹的Person類來冻晤,然后打斷點(diǎn),用po的指令绸吸,得到的如下圖所示鼻弧。其中,Person在添加觀擦者之前的self.person對象的類名和class類方法是一樣的锦茁。
在有注冊了觀察者之后self.person對象的類名變成了NSKVONotifying_Person這一個中間類名了攘轩。
所以中間生成的是一個動態(tài)類
NSKVONotifying_Person
,但是修改的是原對象的isa码俩。
7.2中間類與原類的關(guān)系
對于動態(tài)的生成的中間類NSKVONotifying_Person
與Person
這個類的它們之間的關(guān)系是怎樣的暫時還是不清楚的度帮。為了搞明白它們之間的關(guān)系,就寫了一個方法來探究一下兩者之間的關(guān)系
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
// 注冊類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個數(shù)組稿存, 其中包含給定對象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取所有已注冊的類
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
并且在Person類對屬性注冊觀察者之前和之后分別打印當(dāng)前的注冊的類和子類列表笨篷,得到的結(jié)果
2020-04-18 22:56:39.522750+0800 KVODemo[7411:334483] classes = (
Person
)
2020-04-18 22:56:42.953337+0800 KVODemo[7411:334483] classes = (
Person,
"NSKVONotifying_Person"
)
由此可知,對象Person
與動態(tài)生成的類NSKVONotifying_Person
之間的關(guān)系是繼承關(guān)系瓣履。NSKVONotifying_Person
是Person
類的子類率翅。但是,并不是說KVO是對所有的要被觀察的類的屬性和變量都是可以觀察的監(jiān)聽的袖迎,因?yàn)樵赑erson類中添加成員變量冕臭,并且修改成員變量的值,發(fā)現(xiàn)回調(diào)中并沒有值返回燕锥。
7.2中間類的內(nèi)部
對于動態(tài)生成的中間類NSKVONotifying_xxxx
是不是很好奇內(nèi)部的方法到底是怎樣的辜贵?下面就添加了這個方法來遍歷出類的全部方法
#pragma mark - 遍歷方法
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********************");
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
并且分別在添加觀察name之前和之后打印出來,只對name屬性觀察
[self printClasses:[Person class]];
[self printClassAllMethod:[Person class]];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self printClasses:[Person class]];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
打印的結(jié)果
2020-04-23 10:06:22.678886+0800 KVODemo[3414:59220] classes = (
Person
)
2020-04-23 10:06:28.331048+0800 KVODemo[3414:59220] *********************
2020-04-23 10:06:28.331264+0800 KVODemo[3414:59220] setDateArray:-0x10fcf3bc0
2020-04-23 10:06:28.331381+0800 KVODemo[3414:59220] .cxx_destruct-0x10fcf3c00
2020-04-23 10:06:28.331506+0800 KVODemo[3414:59220] name-0x10fcf3ac0
2020-04-23 10:06:28.331631+0800 KVODemo[3414:59220] setName:-0x10fcf3af0
2020-04-23 10:06:28.331757+0800 KVODemo[3414:59220] dateArray-0x10fcf3ba0
2020-04-23 10:06:28.331873+0800 KVODemo[3414:59220] fullName-0x10fcf39b0
2020-04-23 10:06:28.332025+0800 KVODemo[3414:59220] setFullName:-0x10fcf3a80
2020-04-23 10:06:28.332420+0800 KVODemo[3414:59220] nickName-0x10fcf3b30
2020-04-23 10:06:28.332799+0800 KVODemo[3414:59220] setNickName:-0x10fcf3b60
2020-04-23 10:06:28.336781+0800 KVODemo[3414:59220] classes = (
Person,
"NSKVONotifying_Person"
)
2020-04-23 10:06:28.336950+0800 KVODemo[3414:59220] *********************
2020-04-23 10:06:28.337077+0800 KVODemo[3414:59220] setName:-0x110079c7a
2020-04-23 10:06:28.337197+0800 KVODemo[3414:59220] class-0x11007873d
2020-04-23 10:06:28.337296+0800 KVODemo[3414:59220] dealloc-0x1100784a2
2020-04-23 10:06:28.337383+0800 KVODemo[3414:59220] _isKVOA-0x11007849a
從打印出來的結(jié)果可以看到NSKVONotifing_Person的類的方法分別有class
,delloc
,_isKVOA
和setName:
方法,因?yàn)橹粚ame屬性觀察归形,所以只有setName方法托慨,就是說NSKVONotifing_Person雖然是Person類的子類,但是并不是將Person類的全部方法都加進(jìn)去的连霉,只重寫了觀察屬性的setter方法榴芳。
在delloc方法對被觀察的屬性銷毀之后嗡靡,中間動態(tài)類的isa會重新指向原來的對象,并且當(dāng)生成了一次中間類之后窟感,這個中間類就會一直存在緩存中讨彼,并不會被銷毀的。
8.最后
至此有關(guān)KVO的基礎(chǔ)與原理相關(guān)的就介紹到這里了柿祈,如果想了解更多的詳細(xì)有關(guān)KVO的知識哈误,可以閱讀蘋果的官方文檔鍵值觀察編程指南