本文僅是記錄自己在學習的過程中的理解:如有錯誤,還望各位大佬指正,THX.
KVO全稱KeyValueObserving,是蘋果提供的一套事件通知機制。允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件。由于KVO的實現機制,所以對屬性才會發(fā)生作用,一般繼承自NSObject的對象都默認支持KVO。
KVO和NSNotificationCenter都是iOS中觀察者模式的一種實現。區(qū)別在于,相對于被觀察者和觀察者之間的關系,KVO是一對一的,而一對多的。KVO對被監(jiān)聽對象無侵入性,不需要修改其內部代碼即可實現監(jiān)聽。
KVO可以監(jiān)聽單個屬性的變化,也可以監(jiān)聽集合對象的變化。通過KVC的mutableArrayValueForKey:等方法獲得代理對象,當代理對象的內部對象發(fā)生改變時,會回調KVO監(jiān)聽的方法。集合對象包含NSArray和NSSet。
1. KVO 的基本使用
相信大家在平時的開發(fā)中都使用過KVO,使用KVO分為3個步驟:
1.通過- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法注冊觀察者,觀察者可以接收keyPath屬性的變化事件。
參數部分:
--Observer參數:觀察者對象
--keyPath參數:需要觀察的屬性,由于是字符串的形式,寫錯的話很容易導致崩潰,一般利用系統的反射機制NSStringFromSelector(@selector(keyPath));
--options參數:枚舉類型
NSKeyValueObservingOptionNew 接收新值,默認為只接收新值
NSKeyValueObservingOptionOld 接收舊值
NSKeyValueObservingOptionInitial 在注冊的時候立即接收一次回調,在改變是也會發(fā)生通知
NSKeyValueObservingOptionPrior 改變之前發(fā)一次,改變之后發(fā)一次
--context參數:傳入任意類型的對象,在接收消息回調的代碼中可以接收到這個對象,是KVO中的一種傳值方式
**注意:在調用addObserver方法后,KVO并不會對觀察者進行強引用,所以需要注意觀察者的生命周期,否則會導致由于觀察者的釋放而帶來的崩潰。
2.在觀察者中實現-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,當keyPath屬性發(fā)生改變之后,KVO會回調這個方法來通知觀察者屬性的改變。
3.當觀察者不需要監(jiān)聽的時候,可以調用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法將KVO移除,需要注意的是,調用removeObserver需要在觀察者消失之前,否則會導致崩潰。一般在dealloc中調用。
KVO的addObserver和removeObserver需要是成對的,如果重復remove則會導致NSRangeException類型的Crash,如果忘記remove則會在觀察者釋放后再次接收到KVO回調時Crash。蘋果官方推薦的方式是,在init的時候進行addObserver,在dealloc時removeObserver,這樣可以保證add和remove是成對出現的,是一種比較理想的使用方式。
2. KVO的觸發(fā)模式
KVO在屬性發(fā)生改變的時候默認是自動調用的,如果需要手動的控制這個調用時機,或者自己來實現KVO屬性的調用,可以通過KVO提供的方法來調用。
在所要觀察的對象.m文件中加入:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
return YES;//默認,自動模式
return NO;//手動模式
}
同時在屬性變化之前,調用:
- (void)willChangeValueForKey:(NSString *)key;
在屬性變化之后,調用:
- (void)didChangeValueForKey:(NSString *)key;
其實無論屬性的值是否發(fā)生改變,是否調用Setter方法,只要調用了willChangeValueForKey:和didChangeValueForKey:就會觸發(fā)回調。
一般我們在開發(fā)的時候,需要用到KVO監(jiān)聽屬性值得變化,一般不會將所有的值得監(jiān)聽都是手動的觸發(fā),同時我們也看到automaticallyNotifiesObserversForKey:傳入了一個參數key, 就是為了讓我們根據key來決定是否手動開啟KVO.
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;//手動模式
}
return YES;//默認,自動模式
}
3. KVO屬性依賴
如果在當前Person類中引入另外一個Dog類:
// Dog.h
@interface Dog : NSObject
@property (nonatomic,assign) NSInteger age;
@property (nonatomic,assign) NSInteger level;
@end
// Person.h
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong) Dog *dog;
@end
//Person.m
@implementation Person
-(instancetype)init
{
if (self = [super init]) {
_dog = [[Dog alloc] init];
}
return self;
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;//手動模式
}
return YES;//默認,自動模式
}
那么此時我們怎么通過Person來觀察Dog類的age屬性呢?
[_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];
如果Dog類有多個屬性;那么我們現在希望,只要Dog類中有屬性的變化,就會通知到Person類,如果我們每一個屬性都添加一遍觀察者,是不是很麻煩,那么這里就需要用到屬性依賴:我們在Person類的.m中添加一個方法:
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPath = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqual:@"dog"]) {
keyPath = [[NSSet alloc] initWithObjects:@"_dog.age",@"_dog.level", nil];
}
return keyPath;
}
同時在添加觀察者時,不用對dog具體的屬性添加:
[_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
4. KVO 的原理
KVO的其實就是觀察屬性的變化,也就是setter方法的變化,但是上面我們也提到過就是不需要調用setter方法同樣可以觸發(fā)KVO,那么KVO到底是不是觀察setter方法呢?現在我們把代碼恢復到最初的時候,此時只觀察Person類的name屬性,如果此時把name改成成員變量:
// Person.h
@interface Person : NSObject
{
@public
NSString *name;
}
//@property (nonatomic,copy) NSString *name;
@end
//調用改變name
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int a;
_p.name = [NSString stringWithFormat:@"%d",a++];
}
當改變name的值得時候,可以發(fā)現此時并不會有回調。
那么可以知道,其實KVO觀察的還是屬性的setter方法。
那么如何實現當調用Person類對象的setter方法的時候能夠觀察到改變呢?一般有兩種方式實現:分類和子類繼承。
那么我們可以試一下分類,創(chuàng)建一個Person的分類,并在分類里重寫setName:方法,發(fā)現是可行的。但此時有一個隱患存在,因為此時我們已經在分類中實現了setName:方法,等于就是替換掉了Person類的setName:方法,此時Person類的setName:方法就不會被調用,而此時如果又需要重寫Person類的setName:方法,那么就會出現影響。
在添加觀察者的地方打個斷點來看一下:KVO 底層實現:首先KVO需要創(chuàng)建一個子類(NSKVONotyfing_Person),這個子類是繼承于被觀察對象的,這個子類需要重寫屬性的setter方法,這個時候,外界在調用setter方法的時候,調用的是子類重寫的setter方法。就是讓外界的person對象的isa指針指向這個子類。

。此時Person類對象的isa指針指向的就是子類對象。
5. 自定義KVO
首先創(chuàng)建一個NSObject的分類:
// NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
// NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>
@implementation NSObject (KVO)
-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
//創(chuàng)建一個類
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [@"KVO_" stringByAppendingString:oldClassName];
Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
//注冊類
objc_registerClassPair(MyClass); //MyClass繼承于self.class 根據案例來看,此時MyClass繼承于Person,那么此時MyClass這個子類是否具有父類Person的setName:方法呢? 沒有,只不過我們在調用方法的時候,子類繼承于父類,如果子類沒有實現方法,回去父類中調用該方法,所以在潛意識上,我們人為子類具有父類的方法,所謂的重寫子類的方法,其實就是給這個子類添加一個方法。
//重寫setName方法
class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
//class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
//參數名稱 參數
//Class 給那個類添加方法
//SEL 方法編號
//IMP 方法實現(指針)
//types 返回值類型
//修改isa指針
object_setClass(self, MyClass);
//將觀察者保存到當前對象
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self,SEL _cmd,NSString * newName){
NSLog(@"%@",newName);
//調用父類的setName:方法
Class class = [self class];//拿到當前類型
object_setClass(self, class_getSuperclass(class));//修改當前類型,變成父類
objc_msgSend(self, @selector(setName:),newName);
//拿到Observer,
id observer = objc_getAssociatedObject(self, @"observer");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",@{@"name":newName,@"kind":@1},nil);
}
//改回子類
object_setClass(self, class);
}
@end
這么寫的KVO不會覆蓋父類的set方法,也不會因為沒有在dealloc中remove掉observer而崩潰掉。
6. 容器類的KVO
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong)NSMutableArray * array;
@end
// Person.m
-(NSMutableArray *)array
{
if (!_array) {
_array = [NSMutableArray array];
}
return _array;
}
// ViewController.m
[_p addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
其實注冊觀察者的步驟與屬性時一樣的,只不過在修改array的時候有些變化,因為KVO監(jiān)聽的是set方法,而對array進行操作卻不是set方法,這時候其實KVO提供了一個方法:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray *tempArray = [_p mutableArrayValueForKey:@"array"];
[tempArray addObject:@"xxxx"];
//利用tempArray 去進行操作
}
通過斷點來看一下tempArray的類型:

最后補充幾個注意:
1.kvo的本質是什么?
利用runtimeAPI動態(tài)生成一個子類,并讓instance對象的isa指向這個全新的子類,當修改instance對象的屬性時,會調用willChangeValueForKey和didChangeValueForKey( 在父類原來的setter方法)并調用內部會觸發(fā)監(jiān)聽器的監(jiān)聽方法(observerValueForKeyPath:)。
2.直接修改成員變量會觸發(fā)KVO么?
不會觸發(fā)KVO,添加KVO的Person實例,其實是NSKVONotyfing_Person類,再調用setter方法,不是調用Person的setter方法,而是NSKVONotyfing_Person的setter方法,因為修改成員變量不是setter方法賦值。
3.如果在項目中對Person類進行了監(jiān)聽,也創(chuàng)建了一個NSKVONotifying_Person類,那么會編譯通過么?
編譯通過,因為KVO是運行時刻創(chuàng)建的,并不在編譯時刻,在編譯時刻只有一個NSKVONotifying_Person,所以不報錯,可以通過,但是此時KVO起不了作用。(KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class)