本文分為2個部分:概念
與應用
。概念
部分旨在剖析KVO這一設計模式的實現原理;應用
部分通過建立的專案,以說明KVO技術在iOS開發中所帶來的作用;
如果是作為剛接觸KVO的初學者,可以在瞭解第一部分的基本原理後粗略看幾遍底層實現原理,再認真閱讀第二部分的應用內容“學會”怎麼去使用KVO,往後再慢慢深入瞭解KVO這一“黑魔法”技術的實現原理。
本文Demo下載連結:KVO演示Demo】
[概念部分]
一、KVO是什麼?
KVO 是 Objective-C 對觀察者設計模式的一種實現。【另外一種是:通知機制(notification),詳情參考:iOS 趣談設計模式——通知】;
KVO提供一種機制,指定一個被觀察物件(例如A類),當物件某個屬性(例如A中的字串name)發生更改時,物件會獲得通知,並作出相應處理;【且不需要給被觀察的物件新增任何額外程式碼,就能使用KVO機制】
在MVC設計架構下的專案,KVO機制很適合實現mode模型和view檢視之間的通訊。
例如:程式碼中,在模型類A建立屬性資料,在控制器中建立觀察者,一旦屬性資料發生改變就收到觀察者收到通知,通過KVO再在控制器使用回撥方法處理實現檢視B的更新;(本文中的應用就是這樣的例子.)
二、實現原理?
KVO在Apple中的API文件如下:
1 2 3 4 |
Automatic key-value observing is implemented using a technique called isa-swizzling… 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 … |
KVO 的實現依賴於 Objective-C 強大的 Runtime ,從以上Apple 的文件可以看出蘋果對於KVO機制的實現是一筆帶過,而具體的細節沒有過多的描述,但是我們可以通過Runtime的所提供的方法去探索,關於KVO機制的底層實現原理。為此啊左從網上的一些關於KVO的資料總結了有關的內容:
基本的原理:
當觀察某物件A時,KVO機制動態建立一個物件A當前類的子類,併為這個新的子類重寫了被觀察屬性keyPath的setter 方法。setter 方法隨後負責通知觀察物件屬性的改變狀況。
深入剖析:
Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。當觀察物件A時,KVO機制動態建立一個新的名為: NSKVONotifying_A的新類,該類繼承自物件A的本類,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會負責在呼叫原 setter 方法之前和之後,通知所有觀察物件屬性值的更改情況。
(備註: isa 混寫(isa-swizzling)isa:is a kind of ; swizzling:混合,攪合;)
①NSKVONotifying_A類剖析:在這個過程,被觀察物件的 isa 指標從指向原來的A類,被KVO機制修改為指向系統新建立的子類 NSKVONotifying_A類,來實現當前類屬性值改變的監聽;
所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對KVO的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們建立一個新的名為“NSKVONotifying_A”的類(),就會發現系統執行到註冊KVO的那段程式碼時程式就崩潰,因為系統在註冊監聽的時候動態建立了名為NSKVONotifying_A的中間類,並指向這個中間類了。
(isa 指標的作用:每個物件都有isa 指標,指向該物件的類,它告訴 Runtime 系統這個物件的類是什麼。所以物件註冊為觀察者時,isa指標指向新子類,那麼這個被觀察的物件就神奇地變成新子類的物件(或例項)了。) 因而在該物件上對 setter 的呼叫就會呼叫已重寫的 setter,從而啟用鍵值通知機制。
—>我猜,這也是KVO回撥機制,為什麼都俗稱KVO技術為黑魔法的原因之一吧:內部神祕、外觀簡潔。
②子類setter方法剖析:KVO的鍵值觀察通知依賴於 NSObject 的兩個方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數值的前後分別呼叫2個方法:
被觀察屬性發生改變之前,willChangeValueForKey:被呼叫,通知系統該 keyPath 的屬性值即將變更;當改變發生後,
didChangeValueForKey: 被呼叫,通知系統該 keyPath 的屬性值已經變更;之後, observeValueForKey:ofObject:change:context: 也會被呼叫。且重寫觀察屬性的setter 方法這種繼承方式的注入是在執行時而不是編譯時實現的。
KVO為子類的觀察者屬性重寫呼叫存取方法的工作原理在程式碼中相當於:
1 2 3 4 |
-(void)setName:(NSString *)newName{ [self willChangeValueForKey:@"name"]; //KVO在呼叫存取方法之前總呼叫 [super setValue:newName forKey:@"name"]; //呼叫父類的存取方法 [self didChangeValueForKey:@"name"]; //KVO在呼叫存取方法之後總呼叫} |
三、特點:
觀察者觀察的是屬性,只有遵循 KVO 變更屬性值的方式才會執行KVO的回撥方法,例如是否執行了setter方法、或者是否使用了KVC賦值。
如果賦值沒有通過setter方法或者KVC,而是直接修改屬性對應的成員變數,例如:僅呼叫_name = @”newName”,這時是不會觸發kvo機制,更加不會呼叫回撥方法的。
所以使用KVO機制的前提是遵循 KVO 的屬性設定方式來變更屬性值。
四、步驟
- 1.註冊觀察者,實施監聽;
- 2.在回撥方法中處理屬性發生的變化;
- 3.移除觀察者
[應用部分]
五.實現方法(蘋果API文件中的方法):
A.註冊觀察者:
1 2 3 4 5 6 |
//第一個引數observer:觀察者 (這裡觀察self.myKVO物件的屬性變化) //第二個引數keyPath: 被觀察的屬性名稱(這裡觀察self.myKVO中num屬性值的改變) //第三個引數options: 觀察屬性的新值、舊值等的一些配置(列舉值,可以根據需要設定,例如這裡可以使用兩項) //第四個引數context: 上下文,可以為kvo的回撥方法傳值(例如設定為一個放置資料的字典) [self.myKVO addObserver:self forKeyPath:@"num" options: NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; |
B. 屬性(keyPath)的值傳送變化時,收到通知,呼叫以下方法:
1 2 3 4 5 6 7 |
//keyPath:屬性名稱 //object:被觀察的物件 //change:變化前後的值都儲存在change字典中 //context:註冊觀察者時,context傳過來的值 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { } |
六、上程式碼~:
1.新建專案
UI介面設計如下:
第一個是便籤,用於顯示num數值,關聯ViewController並命名為:label;
第二個是按鈕,用於改變num的數值,關聯ViewController並命名為:changeNum。
2.模型建立
【新建一個File,選擇Cocoa Touch Class,命名為“myKVO”,記得選擇Subclass of “NSObject”.】程式碼如下:
(myKVO.h):
1 2 3 |
@interface myKVO : NSObject @property (nonatomic,assign)int num; //屬性設定為int型別的 num@end |
(myKVO.m):
1 2 3 4 |
#import "myKVO.h" @implementation myKVO @synthesize num; @end |
3.在ViewController中監聽並響應屬性改變。
(ViewController.h):
1 2 3 4 5 |
#import @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet UILabel *label;//便籤label - (IBAction)changeNum:(UIButton *)sender; //按鈕事件 @end |
(ViewController.m):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#import "ViewController.h" #import "myKVO.h" @interface ViewController () @property (nonatomic,strong)myKVO *myKVO; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.myKVO = [[myKVO alloc]init]; /*1.註冊物件myKVO為被觀察者: option中, NSKeyValueObservingOptionOld 以字典的形式提供 “初始物件資料”; NSKeyValueObservingOptionNew 以字典的形式提供 “更新後新的資料”; */ [self.myKVO addObserver:self forKeyPath:@"num" options: NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; } /* 2.只要object的keyPath屬性發生變化,就會呼叫此回撥方法,進行相應的處理:UI更新:*/ -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ // 判斷是否為self.myKVO的屬性“num”: if([keyPath isEqualToString:@"num"] & object == self.myKVO) { // 響應變化處理:UI更新(label文字改變) self.label.text = [NSString stringWithFormat:@"當前的num值為:%@", [change valueForKey:@"new"]]; //change的使用:上文註冊時,列舉為2個,因此可以提取change字典中的新、舊值的這兩個方法 NSLog(@"\noldnum:%@ newnum:%@",[change valueForKey:@"old"], [change valueForKey:@"new"]); } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; /* 3.移除KVO */ [self removeObserver:self forKeyPath:@"num" context:nil]; } //按鈕事件 - (IBAction)changeNum:(UIButton *)sender { //按一次,使num的值+1 self.myKVO.num = self.myKVO.num + 1; } @end |
除錯:便籤label初始化沒有數值,當每次點選按鈕後,label記錄的num隨之增加,表明按鈕使屬性num增加的同時,KVO機制傳送通知,並呼叫observeValueForKeyPath:方法使UI更新。(本文Demo下載連結:KVO演示Demo)
七、擴充–>
1.KVC與KVO的不同?
KVC(鍵值編碼),即Key-Value Coding,一個非正式的Protocol,使用字串(鍵)訪問一個物件例項變數的機制。而不是通過呼叫Setter、Getter方法等顯式的存取方式去訪問。
KVO(鍵值監聽),即Key-Value Observing,它提供一種機制,當指定的物件的屬性被修改後,物件就會接受到通知,前提是執行了setter方法、或者使用了KVC賦值。
2.和notification(通知)的區別?
notification比KVO多了傳送通知的一步。
兩者都是一對多,但是物件之間直接的互動,notification明顯得多,需要notificationCenter來做為中間互動。而KVO如我們介紹的,設定觀察者->處理屬性變化,至於中間通知這一環,則隱祕多了,只留一句“交由系統通知”,具體的可參照以上實現過程的剖析。
notification的優點是監聽不侷限於屬性的變化,還可以對多種多樣的狀態變化進行監聽,監聽範圍廣,例如鍵盤、前後臺等系統通知的使用也更顯靈活方便。
(參照通知機制第五節系統通知名稱內容)
3.與delegate的不同?
和delegate一樣,KVO和NSNotification的作用都是類與類之間的通訊。但是與delegate不同的是:
這兩個都是負責傳送接收通知,剩下的事情由系統處理,所以不用返回值;而delegate 則需要通訊的物件通過變數(代理)聯絡;
delegate一般是一對一,而這兩個可以一對多。
4.涉及技術:
KVC/KVO實現的根本是Objective-C的動態性和runtime,以及訪問器方法的實現;
總結:
對比其他的回撥方式,KVO機制的運用的實現,更多的由系統支援,相比notification、delegate等更簡潔些,並且能夠提供觀察屬性的最新值以及原始值;但是相應的在建立子類、重寫方法等等方面的記憶體消耗是很巨大的。所以對於兩個類之間的通訊,我們可以根據實際開發的環境採用不同的方法,使得開發的專案更加簡潔實用。
另外需要注意的是,由於這種繼承方式的注入是在執行時而不是編譯時實現的,如果給定的例項沒有觀察者,那麼KVO不會有任何開銷,因為此時根本就沒有KVO程式碼存在。但是即使沒有觀察者,委託和NSNotification還是得工作,這也是KVO此處零開銷觀察的優勢。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式