1.1 什麼是KVO?html
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.(鍵值觀察是一種機制,容許對象在其餘對象的指定屬性發生更改時獲得通知)
安全
Important: In order to understand key-value observing, you must first understand key-value coding.(重要提示:要了解鍵值觀察,必須首先了解鍵值編碼.)性能優化
附:上篇關於KVC探索的地址 juejin.im/post/5e4509….bash
2.1 常見函數中參數的做用app
self.person = [LGPerson new];
self.student = [LGStudent shareInstance];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
複製代碼
在以前的開發中,context 咱們通常都傳nil,或者傳NULL.(沒特殊狀況下,最好傳NULL,由於context類型是 void *).可是在比較複雜的狀況下,context有什麼做用呢?咱們看下蘋果開發文檔是怎麼說的,關於蘋果開發文檔在上篇文章中也有介紹,這裏就不作使用介紹了,直奔主題.ide
The context pointer in the addObserver:forKeyPath:options:context:
message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL
and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.函數
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.post
The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the balance
and interestRate
properties chosen this way.性能
大體瞭解下是啥意思:您能夠指定NULL並徹底依賴於密鑰路徑字符串來肯定更改通知的來源,可是這種方法可能會致使對象出現問題,該對象的超類因爲不一樣的緣由也在觀察相同的密鑰路徑, 一種更安全、更可擴展的方法是使用上下文來確保接收到的通知是發送給觀察者的,而不是一個超類.
優化
當子類和父類同時觀察類中的某個屬性的時候,context 能夠更好的進行區分.好比看代碼裏:
LGStudent是LGPerson的子類,有着相同屬性name.在當屬性發生改變時,當前的ViewController會獲得相應的回調:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@",change);
}
複製代碼
若是按以前Context傳NULL的寫法:
if (object == self.student) {
if ([keyPath isEqualToString:@"name"]) {
}else if ([keyPath isEqualToString:@"nick"]){
}
}else if (object == self.person){
if ([keyPath isEqualToString:@"name"]) {
}else if ([keyPath isEqualToString:@"nick"]){
}
}
複製代碼
若是代碼中傳了context,代碼以下:
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
static void *StundentNameContext = &StundentNameContext;
複製代碼
// OC -> c 超集
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:StundentNameContext];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
}else if (context == PersonNameContext){
}else if (context == StundentNameContext){
}
NSLog(@"LGViewController - %@",change);
}
複製代碼
咱們在代理裏是否是能夠用context判斷相應對象的屬性變化,是一種更安全、更可擴展的方法.
2.2 在頁面銷燬的時候(dealloc時)移除當前被對象觀察的屬性
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nick"];
[self.student removeObserver:self forKeyPath:@"name"];
// [self.timer invalidate];
// self.timer = nil;
}
複製代碼
爲何要移除觀察者呢?
2.3 "自動擋" 與 "手動擋"
什麼是"手動擋" 和 "自動擋"?
蘋果開發文檔上介紹:Manual change notification provides additional control over when notifications are emitted, and requires additional coding. You can control automatic notifications for properties of your subclass by implementing the class method automaticallyNotifiesObserversForKey.(手動更改通知提供了對什麼時候發出通知的額外控制,而且須要額外的編碼。經過實現類方法automaticallyNotifiesObserversForKey:,能夠控制子類屬性的自動通知)
NSObject provides a basic implementation of automatic key-value change notification. Automatic key-value change notification informs observers of changes made using key-value compliant accessors, as well as the key-value coding methods. Automatic notification is also supported by the collection proxy objects returned by, for example, mutableArrayValueForKey.(NSObject提供了自動鍵值更改通知的基本實現。自動鍵值更改通知通知觀察員使用鍵值兼容訪問器所作的更改,以及鍵值編碼方法。由mutableArrayValueForKey返回的集合代理對象也支持自動通知。 清單1所示的示例將致使屬性名的任何觀察者收到更改通知)
那怎麼觸發」手動擋「和」自動擋「呢?
」自動擋「:
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
複製代碼
看上去是否是有些眼熟?沒錯,KVC的基本調用.原來KVC的調用過程就會自動觸發鍵值觀察(KVO).因此說要了解鍵值觀察,必須首先了解鍵值編碼.
」手動擋「:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
複製代碼
要實現手動觀察者通知,請在更改值以前調用willChangeValueForKey,在更改值以後調用didChangeValueForKey。
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
複製代碼
好的 ,好的 咱們來一塊兒驗證下:
ViewController裏:
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:StundentNameContext];
// 1: context -- 多個對象 - 相同keypath
// 更加便利 - 更加安全 - 直接
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
}else if (context == PersonNameContext){
}else if (context == StundentNameContext){
}
NSLog(@"LGViewController - %@",change);
}
複製代碼
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"null";
[self.person setValue:@"xiang" forKey:@"nick"];
self.student.name = @"森海北語";
// // KVO 創建在 KVC
// [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
}
複製代碼
LGPerson裏:
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
複製代碼
看下運行結果:
接着:
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
複製代碼
再看下運行結果:
什麼都沒打印?說明automaticallyNotifiesObserversForKey 確實能夠控制子類屬性自動通知開關.
可是把自動開關關閉以後,依然想接收到指定屬性發生更改時獲得通知,該咋辦呢?
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]||[key isEqualToString:@"nick"]) {
return YES;
}
return NO;
}
複製代碼
咱們能夠如上面代碼所示對Key進行判斷,另外個方法以下(更改值以前調用willChangeValueForKey,在更改值以後調用didChangeValueForKey):
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
複製代碼
咱們再看下蘋果開發文檔上的介紹:
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa
pointer to determine class membership. Instead, you should use the class
method to determine the class of an object instance.
自動鍵值觀察是使用一種叫作isa swizzing的技術實現的。 顧名思義,isa指針指向維護分派表的對象類。這個分派表本質上包含指向類實現的方法的指針以及其餘數據。 當觀察者註冊一個對象的屬性時,觀察對象的isa指針被修改,指向一箇中間類,而不是真類。所以,isa指針的值不必定反映實例的實際類。 決不能依賴isa指針來肯定類成員身份。相反,您應該使用類方法來肯定對象實例的類.
在這裏不少盆友不太理解,what’t is ? 這是啥?什麼是isa swizzing, 這個中間類是什麼類?話很少說,直接擼代碼.
3.1 isa swizzing 與 中間類
LGPerson類:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
- (void)sayLove;
@end
複製代碼
#import "LGPerson.h"
@implementation LGPerson
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
- (void)sayHello{
}
- (void)sayLove{
}
@end
複製代碼
LGStudent裏:
#import "LGStudent.h"
@implementation LGStudent
- (void)sayHello{
}
@end
複製代碼
#import "LGStudent.h"
@implementation LGStudent
- (void)sayHello{
}
@end
複製代碼
好的,準備工做作好了,建立兩個類,LGPerson 和 LGStudent.
而後建立個LGViewController ,由工程裏系統建立的 ViewController 跳轉進入的.
LGViewController :
- (void)printClasses:(Class)cls{} 是 遍歷類以及子類的方法
- (void)printClassAllMethod:(Class)cls{} 是 遍歷類裏面的方法-ivar-property
咱們在53行打個斷點,運行代碼:
遍歷LGPerson的類和子類是LGPerson 和 LGStudent .也打印了LGPerson.h裏的方法。
而後咱們再用LLDB指令打印下:
沒問題吧接下來在55行再打個斷點:
而後咱們再用LLDB指令打印下:
個人天,NSKVONotifying_LGPerson 這個是什麼東西喲!!!咱們如今再看剛剛提到的:
當觀察者註冊一個對象的屬性時,觀察對象的isa指針被修改,指向一箇中間類,而不是真類。所以,isa指針的值不必定反映實例的實際類。 決不能依賴isa指針來肯定類成員身份。
原來 NSKVONotifying_LGPerson 這個是個系統幫咱們建立的派生類,前綴是NSKVONotifying_,self.person 對象的ISA 指向了這個派生類。
那麼這個派生類和 LGPerson 有什麼關係呢?咱們把斷點打在56行,看輸出:
原來這個派生類 是 LGPerson 的子類.
3.2 NSKVONotifying_LGPerson 派生子類裏作了什麼?
來到這咱們好好思考下,蘋果爲啥要註冊一個對象的屬性時建立個派生子類,這個子類有啥做用呢?其實在類結構裏,最長用到的就是類結構裏的bits裏的data,這裏面是關於類裏面的實例方法,實例變量... 而方法、變量就是咱們最經常使用到的.咱們在59行裏打上斷點,看看NSKVONotifying_LGPerson派生子類裏 的方法:
原來動態子類重寫了不少方法 setNickName (setter)、 class、 dealloc、 _isKVOA這些方法.
爲啥會重寫setter 方法呢?你們一塊兒思考下...原來重寫setter 方法 對屬性 nickName 作了修改被 KVO
監聽到了.
而爲啥要重寫 class 方法呢?還記得這個圖嗎?
對象的isa指針被修改,指向一個中間類,而不是真類,那麼這時候 po class_getName([self.person class]) 按道理 也應該返回 NSKVONotifying_LGPerson ,爲何 返回LGPerson 呢 、是由於NSKVONotifying_LGPerson 類 重寫了 class 方法 、返回了它的父類 LGPerson .
至於 dealloc、 _isKVOA 這兩個方法 在後面 KVO的自定義裏會聊到.
3.3 對象 ISA 什麼時候指回來?
當觀察者註冊一個對象的屬性時,觀察對象的isa指針被修改,指向一箇中間類,而不是真類,那麼這個觀察對象的isa指針什麼時候會指回來?難道永遠都指向這個中間類了嗎?
其實你們思考下,就能夠得出答案.爲啥isa指針被修改,由於觀察者註冊一個對象的屬性。
那我不觀察時(不用了移除時)不就指回來了嘛。
po object_getClassName(self.person) 是否是指回來了 指回了 LGPerson .
3.4 NSKVONotifying_LGPerson派生子類 會被銷燬嗎?
咱們從LGViewController 回到 ViewController 裏 ,遍歷LGPerson類以及子類:
看下控制檯:
NSKVONotifying_LGPerson 這個 派生子類 是否是依然存在.其實也好理解動態建立類畢竟是個耗時操做、蘋果公司特別注重性能優化這方面,不可能不用時就把這個派生子類給銷燬,那下次進來 觀察者註冊一個對象的屬性時,豈不是要再建立派生子類.
一、當對對象A進行KVO觀察時候,會動態生成一個子類,而後將對象的isa指向新生成的子類
二、KVO本質上是監聽屬性的setter方法,只要被觀察對象有成員變量和對應的set方法,就能對該對象經過KVO進行觀察
三、子類會重寫父類的set、class、dealloc、_isKVOA方法
四、當觀察對象移除全部的監聽後,會將觀察對象的isa指向原來的類
五、當觀察對象的監聽所有移除後,動態生成的類不會註銷,而是留在下次觀察時候再使用,避免反覆建立中間子類