KVO,ioskvo
1.KVO概念
KVO即索引值觀察,它提供一種機制,當被觀察的對象的屬性發生改變後,對象會接收到通知,從而做出相應的改變。
2.KVO實現原理
這裡要說一個isa指標,在Objective-C中,任何類的定義都是對象。類和類的執行個體(對象)沒有任何本質上的區別。任何對象都有isa指標。
那麼什麼是類呢?在xcode中用快速鍵Shift+Cmd+O 開啟檔案objc.h 能看到類的定義:
可以看出:
Class 是一個 objc_class 結構類型的指標, id是一個 objc_object 結構類型的指標.
我們再來看看 objc_class 的定義:
稍微解釋一下各個參數的意思:
isa:是一個Class 類型的指標. 每個執行個體對象有個isa的指標,他指向對象的類,而Class裡也有個isa的指標, 指向meteClass(元類)。元類儲存了類方法的列表。當類方法被調用時,先會從本身尋找類方法的實現,如果沒有,元類會向他父類尋找該方法。同時注意的是:元類(meteClass)也是類,它也是對象。元類也有isa指標,它的isa指標最終指向的是一個根元類(root meteClass).根元類的isa指標指向本身,這樣形成了一個封閉的內迴圈。
super_class:父類,如果該類已經是最頂層的根類,那麼它為NULL。
version:類的版本資訊,預設為0
info:供運行期使用的一些位標識。
instance_size:該類的執行個體變數大小
ivars:成員變數的數組
再來看看各個類執行個體變數的繼承關係:
每一個對象本質上都是一個類的執行個體。其中類定義了成員變數和成員方法的列表。對象通過對象的isa指標指向類。
每一個類本質上都是一個對象,類其實是元類(meteClass)的執行個體。元類定義了類方法的列表。類通過類的isa指標指向元類。
所有的元類最終繼承一個根元類,根元類isa指標指向本身,形成一個封閉的內迴圈。
原理:每一個對象都有一個isa指標,這個對象根據isa指標去尋找它所歸屬的類,當我們給一個對象註冊觀察者的時候,系統會在運行時給這個對象建立一個子類,這個子類繼承於當前對象歸屬的類,並把當前對象的isa指標指向這個子類,於是當前對象就變成了這個子類的一個執行個體。那麼這個子類內部做了什麼操作呢?其實這個子類重寫了set方法,當原對象在調用set方法賦值的時候,會根據isa指標到建立子類的方法列表去尋找set方法的IMP,此時這個重寫的set方法會對所有觀察這個屬性的對象發出通知,於是原有的對象會作出改變。
深入剖析:
Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。當觀察對象A時,KVO機制動態建立一個新的名為: NSKVONotifying_A的新類,該類繼承自對象A的本類,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會負責在調用原 setter 方法之前和之後,通知所有觀察對象屬性值的更改情況。
- NSKVONotifying_A類剖析:在這個過程,被觀察對象的 isa 指標從指向原來的A類,被KVO機制修改為指向系統新建立的子類 NSKVONotifying_A類,來實現當前類屬性值改變的監聽;
- 所以當我們從應用程式層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對KVO的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們建立一個新的名為“NSKVONotifying_A”的類(),就會發現系統運行到註冊KVO的那段代碼時程式就崩潰,因為系統在註冊監聽的時候動態建立了名為NSKVONotifying_A的中間類,並指向這個中間類了。
- 因而在該對象上對 setter 的調用就會調用已重寫的 setter,從而啟用索引值通知機制。
- KVO索引值觀察依賴於NSObject的兩個方法:willChangeValueForKey和didChangevlueForKey,即在索引值改變前後分別調用這兩個方法,然後在這兩個方法的中間調用父類set方法賦值。
- 被觀察屬性發生改變之前,willChangeValueForKey:被調用,通知系統該 keyPath 的屬性值即將變更;當改變發生後, didChangeValueForKey: 被調用,通知系統該 keyPath 的屬性值已經變更;之後observeValueForKey:ofObject:change:context: 也會被調用。且重寫觀察屬性的setter 方法這種繼承方式的注入是在運行時而不是編譯時間實現的。
KVO為子類的觀察者屬性重寫調用存取方法的工作原理在代碼中相當於:
1 -(void)setName:(NSString *)newName2 {3 [self willChangeValueForKey:@"name"]; //KVO在調用存取方法之前總調用4 [super setValue:newName forKey:@"name"]; //調用父類的存取方法5 [self didChangeValueForKey:@"name"]; //KVO在調用存取方法之後總調用6 }
樣本驗證
1 //Person類 2 @interface Person : NSObject 3 @property (nonatomic,copy) NSString *name; 4 @end 5 6 //controller 7 Person *per = [[Person alloc]init]; 8 //斷點1 9 [per addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];10 //斷點211 per.name = @"小明";12 [per removeObserver:self forKeyPath:@"name"];13 //斷點3
View Code
運行項目,
(lldb) po [per class]Person(lldb) po object_getClass(per)NSKVONotifying_Person(lldb)
(lldb) po [per class]Person(lldb) po object_getClass(per)Person(lldb)
上面的結果說明,在per對象被觀察時,framework使用runtime動態建立了一個Person類的子類NSKVONotifying_Person,而且為了隱藏這個行為,NSKVONotifying_Person重寫了- class方法返回之前的類,就好像什麼也沒發生過一樣。但是使用object_getClass()時就暴露了,因為這個方法返回的是這個對象的isa指標,這個指標指向的一定是個這個對象的類對象
3.KVO的特點
由於KVO內部實現的原理是重寫了set方法,因此只有當被觀察對象的屬性調用set方法賦值的時候才會執行KVO的的回調方法。所以如果直接對屬性的成員變數直接賦值那麼不會觸發KVO。
4.KVO的調用步驟
1.註冊觀察者
2.在回調方法中處理事件
3.移除觀察者
5.代碼實踐
1 self.changeStr = @"您好"; 2 [self addObserver:self forKeyPath:@"changeStr" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil]; 3 self.changeStr = @"大家都好"; 4 5 6 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 7 { 8 NSLog(@"被改變的屬性是%@",keyPath); 9 NSString *str = [change objectForKey:NSKeyValueChangeNewKey];10 NSString *odlStr = [change objectForKey:NSKeyValueChangeOldKey];11 NSLog(@"舊屬性是%@",odlStr);12 NSLog(@"新屬性是%@",str);13 }
View Code
輸出結果:
一個Demo: 在LYXItem.h檔案
1 #import <Foundation/Foundation.h>2 3 @interface LYXItem : NSObject4 5 @property(nonatomic, copy) NSString *name;6 @property(nonatomic, copy) NSString *price;7 8 @end
在LYXItemView.h檔案
1 #import <Foundation/Foundation.h> 2 #import "LYXItem.h" 3 4 @interface LYXItemView : NSObject 5 6 @property(nonatomic, weak) LYXItem *item; 7 8 - (void) showItemInfo; 9 10 @end
在LYXItemView.m中
1 #import "LYXItemView.h" 2 3 @implementation LYXItemView 4 5 @synthesize item = _item; 6 7 - (void)showItemInfo 8 { 9 NSLog(@"item名為:%@, 價格為: %@",self.item.name, self.item.price);10 }11 12 13 - (void)setItem:(LYXItem *)item14 {15 self -> _item = item;16 //為item添加監聽器,監聽item的name的屬性的變化17 [self.item addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];18 19 [self.item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil];20 }21 22 23 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context24 {25 NSLog(@"---------------------------observeValueForKeyPath------------------------");26 NSLog(@"被修改的keyPath為:%@",keyPath);27 NSLog(@"被修改的對象為:%@",object);28 NSLog(@"新被修改的屬性值是:%@",[change objectForKey:@"new"]);29 NSLog(@"被修改的上下文是:%@",context);30 }31 32 33 @end
View Code
在運行檔案中
1 LYXItem *item = [[LYXItem alloc] init]; 2 item.name = @"IOS"; 3 item.price = @"6888"; 4 5 LYXItemView *lyxView = [[LYXItemView alloc] init]; 6 lyxView.item = item; 7 [lyxView showItemInfo]; 8 9 // 更改item的值,觸發監聽器的方法10 item.name = @"Android";11 item.price =@"1999";
列印結果: