KVO講解
最近一直在寫swift專案,沒有時間更新自己的技術部落格,以前在部落格裡面寫過KVO的底層原理,今天我們來看一下KVO的整個使用過程和使用場景(附有demo),大約花大家10-15分鐘時間,希望大家看完部落格之後對KVO的使用有更清醒的認識。
下面我們按照以下提綱講解KVO。
- KVO的基本使用
- KVO的觸發模式
- KVO的屬性依賴
- KVO的原理探究
- 自定義KVO
- KVO對容器類的監聽
一、KVO的基本使用
1.基本步驟
- 通過addObserver:forKeyPath:options:context:註冊觀察者,觀察者可以接收keyPath屬性的變化事件。
- 在觀察者中實現observeValueForKeyPath:ofObject:change:context方法,當keyPath屬性發生改變後,KVO會回撥這個方法來通知觀察者。
- 當觀察者不需要監聽時,可以呼叫removeObserve:forKeyPath方法將KVO移除,需要注意的是,呼叫removeObserve需要在觀察者消失之前,否則會導致Crash。
在註冊觀察者時,可以傳入下列引數:
- Observer引數,觀察者物件
- keyPath引數 需要觀察者的屬性,由於是字串形式,如果傳錯格式,容易導致Crash。一旦利用系統的反射機制NSStringFromSelector(keyPath)
- options引數 引數是一個列舉型別
- NSKeyValueObserveOptionNew 接收新值,預設為只接受新值
- NSKeyValueObserveOptionOld 接收舊值
- NSKeyValueObserveOptionInitial 在註冊時接收一次回撥,在改變時也會發送通知
- NSKeyValueObserveOptionPrior 改變之前發一次,改變之後發一次
- Context引數 傳入任意型別的物件,在接收訊息回撥的程式碼中科院接收到這個物件,是KVO中的一種傳值方式。
2.案例操作
2.1 新建專案叫:KVO的基本使用
2.2 demo程式碼
新增人-age屬性,如下:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject @property (nonatomic,assign)int age; @end NS_ASSUME_NONNULL_END #import "Person.h" @implementation Person @end
2.3 在ViewController實現
2.3.1 匯入Person類,並建立類物件
2.3.2 在ViewDidLoad中建立類物件,並註冊觀察者
在下面觀察屬性值變化實現方法observeValueForKeyPath
最後要移除觀察者removeObserver
我們加入響應事件touchesBegan,每次點選頁面,age都會自動加1
下面是整個的程式碼ViewController的程式碼
#import "ViewController.h" #import "Person.h" @interface ViewController () @property(nonatomic,strong) Person * p; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; _p = [[Person alloc]init]; //註冊 [_p addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) 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 int a; _p.age = a++; } - (void)dealloc{ [_p removeObserver:self forKeyPath:NSStringFromSelector(@selector(age))]; } @end
2.4 程式碼測試
點選了介面三次,列印結果如下:
發現new值在不斷地增加,滿足了監聽person的age屬性的要求。
>>>>拓展
上面程式碼[p addObserver:self],而在控制器中宣告p物件用的屬性修飾詞是Strong,這其中中這裡面有沒有強引用關係?(p有沒有強引用self)
在呼叫addObserver方法後,KVO並不會對觀察者進行強引用,所以我們要注意觀察者的生命週期,因為[p addObserver:self]中,一旦self(控制器)銷燬的時候,p也就是拿不到self,那麼剩下一個問題,p會不會呼叫 observeValueForKeyPath呢,答案是仍然是會呼叫,而給已經釋放的記憶體傳送一個訊息,接下來會發生crash。
所以要在dealloc方法中,移除觀察者。
二、KVO的觸發模式
KVO在屬性發生改變的時候呼叫是自動的,如果想要手動控制這個呼叫時機,或者自己實現KVO屬性的呼叫,則可以通過KVO提供的方法進行呼叫。
如果想手動控制,可以實現下面方法
當我們再次點選螢幕,發現控制檯無任何的列印結果。如果想要監聽結果,需要在響應事件中加入willChangeValueForKey和didChangeValueForKey方法,如下圖
經過加入方法之後,控制檯重新出現列印結果如下
>>>>拓展
如果沒有改變age屬性的值,還能觸發KVO嘛,也就是註釋掉 _p.age = a++; (去掉setter方法)還能觸發嘛?
我們執行程式碼,發現還會執行 (只要實現了willChangeValueForKey和didChangeValueForKey方法)
三、KVO的屬性依賴
3.1 案例分析
如果新增一個類Dog,而Dog也新增一屬性age,如下圖
同時將Dog類作為Person的一個物件
Person並初始化Dog物件
在ViewController中, 註冊監聽者不能使用NSStringFromSelector方法了 ,應該使用字串了
然後點選螢幕進行觸控事件點選
在控制檯進行列印結果如下:

3.2 案例拓展
新增一需求,如果Dog類屬性特別多,我們有一個需求,只要Dog類的任一屬性發生改變,就通知Dog類的觀察者?
在Dog類中加入level屬性,只要Dog類的屬性發生改變,通知觀察者
Dog類新增屬性level等級
而在KVO本身的封裝程式碼中,有一個方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
NSSet是一個集合,返回所有的屬性,方法如下:
四、KVO原理探究
對於KVO的原理探究的文章,本人已經寫好了,請看一下部落格
https://www.cnblogs.com/guohai-stronger/p/9473551.html
五、自定義KVO
下面我們自己寫KVO,在寫之前,我們首先看一下蘋果自身怎麼實現的?比較關鍵的一個方法是
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
如果檢視原始碼發現,蘋果是建立NSObject分類Category來實現KVO的
下面我們就自定義KVO。
5.1 建立分類XY_KVO
5.1.1 自定義一個方法:
- (void)XY_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
5.1.2 實現方法
- (void)XY_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{ //1.建立一個類 NSString *oldClassName = NSStringFromClass(self.class); NSString *newClassName = [@"XYKVO" stringByAppendingString:oldClassName]; /**myClass的父類是person類*/ Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0); //註冊類 objc_registerClassPair(myClass); //2.重寫set方法(所謂的重寫是新增set方法,如果不重寫set方法,子類是沒有set方法的(父類是Person類),但子類是可以呼叫set方法的) //class_addMethod(<#Class_Nullable __unsafe_unretained cls#>, <#SEL_Nonnull name#>, <#IMP_Nonnull imp#>, <#const char * _Nullable types#>) /** *Class 給那個類新增方法 *SEL 方法編號 *IMP 方法實現 *types 返回值型別 */ /** v@:@代表返回值為void,第一個引數@代表呼叫者,第二個:代表方法編號也就是方法名字,第三個代表傳參(真正代表你傳參的) */ class_addMethod(myClass, @selector(setAge:), (IMP)setAge, "v@:@"); //3.修改isa指標!!isa指標指向子類 object_setClass(self, myClass); } void setAge(id self,SEL _cmd,NSString *age){ NSLog(@"來了"); }
拓展》》》
class_addMethod(myClass, @selector( setAge: ), (IMP)setAge, " v@:@ ");中setAge:明明只有一個引數,為什麼返回的要有三個引數如果不寫,就會返回物件原因?
舉例:
_p = [Person alloc];
_p = [_p init];
將init這句程式碼改為_p = objc_msgSend(_p,@selector(init))
(任何oc的方法呼叫都會變為objc_msgSend(_p,@selector(方法)))-訊息傳送,第一個引數是方法物件,第二個方法編號名字
5.1.3 測試過程
1.匯入類NSObject+XYKVO.h
2.使用自定義的KVO
3.觸碰時改變age值
4.然後檢視自定義KVO裡面set方法,看是否打印出“來了”
結果出現“來了”,說明自定義KVO實現監聽啦
5.3 監聽屬性
5.3.1 將觀察者儲存到當前物件
5.3.2 傳送監聽通知
void setAge(id self,SEL _cmd,NSString *age){ NSLog(@"來了"); //呼叫父類的set方法 Class class = [self class];//子類當前型別 object_setClass(self, class.getSuperclass(class)); objc_msgSend(self,@selector(setAge:),age) // //拿到觀察者之後,要傳送通知 id observer = objc_getAssociatedObject(self, @"observer"); if (observer) { objc_msgSend(observer,@selector(observeValueForKeyPath:ofObject:change:context:),@"age",self,@{@"new":age,@"kind":@1},nil); } object_setClass(self, class); }
通過objc_msgSend(observer,@selector(observeValueForKeyPath:ofObject:change:context:),@"age",self,@{@"new":age,@"kind":@1},nil);就可以實現監聽在控制器中
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); }
六、 KVO對容器類的監聽
6.1 新增容器類屬性並初始化
初始化
6.2 對容器類新增監聽
6.3 觸碰螢幕檢視容器屬性變化
如果上面的容器使用註釋的那行[_p.arr addObject:@"11"],會觸發KVO嘛?
答案是 不會 ,因為addObject不是set方法,KVO通過set方法來觸發,而蘋果專門給KVO提供個介面,通過mutableArrayValueForKey方法,來觸發 。
6.4 結果
發現tempArr是NSKeyValueNotifyingMutableArray,這個類是NSMutableArray的子類,我們可以聯想到容器類監聽和屬性監聽差不多,有著異曲同工之處。 請大家細細體會。
上面就是關於KVO的基本講解,以後會慢慢的剖解更多OC的底層知識,供大家閱讀。