1.什么是觀察者模式?
2.為什么要用觀察者模式?它的優缺點是什么?
![Uploading 屏幕快照 2016-12-20 下午3.23.56_660666.png . . .]
3.觀察者模式解決了什么問題?你的項目中哪里用到了觀察者模式?
4.觀察者模式怎么用?有幾種方式?
5.怎么創建觀察者模式?
7.寫觀察者模式的時候需要注意什么問題?
8.iOS源代碼中哪里用到了觀察者模式?舉例說明。
9.實現原理,
根據上面的套路,我們一一回答問題。
1.什么是觀察者模式
KVO 是 Objective-C 對觀察者設計模式的一種實現。【另外一種是:通知機制(notification)
KVO提供一種機制,指定一個被觀察對象(例如A類),當對象某個屬性(例如A中的字符串name)發生更改時,對象會獲得通知,并作出相應處理;【且不需要給被觀察的對象添加任何額外代碼,就能使用KVO機制】
在MVC設計架構下的項目,KVO機制很適合實現mode模型和view視圖之間的通訊。
例如:代碼中,在模型類A創建屬性數據,在控制器中創建觀察者,一旦屬性數據發生改變就收到觀察者收到通知,通過KVO再在控制器使用回調方法處理實現視圖B的更新;(本文中的應用就是這樣的例子.)
簡單的說就是,當某對象改變時,自動通知所有相關的狀態進行更新。
2.為什么要用觀察者模式
觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
觀察者模式的優點:
1、 Subject和Observer之間是松偶合的,分別可以各自獨立改變。
2、Subject在發送廣播通知的時候,無須指定具體的Observer,Observer可以自己決定是否要訂閱Subject的通知。
3、高內聚、低偶合。
3.觀察者模式解決了什么問題?你的項目中哪里用到了觀察者模式?
有時候我們需要監聽某個類的屬性值的變化從而做出相應的改變,這個時候使用KVO/KVC設計模式。
比如,在項目中,我需要監聽model中的某個屬性值的變換,當變化時,需要更新UI顯示。
//增加frame的監聽
1)用于判斷當前手勢滑動的frame moveTableView:didChangeFromFrame:toFrame
2)增加搜索結果contentview 根據手勢切換導航顯示模式
[self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
4.觀察者模式怎么用?有幾種方式?
在iOS中觀察者模式的實現有四種方法:NSNotification、KVO、Protocol以及Code Block代碼塊。
要點:
Notification是一對多的,而delegate回調是一對一的。
Notification - NotificationCenter機制使用了操作系統的對象間通訊功能,而delegate是直接的函數調用。Notification跨度大,而delegate效率可能比較高。
相較于前兩者KVO才是一種真正的觀察者模式,它允許你將一個處理函數綁定到某個類的屬性,屬性發生改變是就會自動觸發,不像其他兩種需要你手動的發通知。KVO是一種非常靈活的觀察機制,廣泛應用于界面設計。
Code Block其實就相當于C的函數指針,可以用來做各種回調。我覺得其應當具備最高的效率。使用Code Block要注意的地方就是使用外部變量。在block里直接引用外部變量的話會在block定義的時候復制外部變量的一個拷貝,也就是說得到的是block定義時的值,在block內修改這個值也不會傳給外部。要得到實時的數據,或者將數據傳出的話需要在相關變量前面加__block即可。
NSNotification
發送通知:
實現通知的方法
KVO
//注冊通知
[_tableView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
//通知回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"frame"]) {
//處理邏輯
}
}
//移除通知
- (void)dealloc
{
[_tableView removeObserver:self forKeyPath:@"frame"];
}
protocol/delegate
需要注意的問題:
1)A對象要通知B對象,B對象必須實現監聽的方法,否則一旦有消息發送就會導致崩潰.
- A對象不想通知B對象了,需要從B對象身上移除掉通知.
但是要注意:添加的觀察者的次數要和移除觀察者的次數相等,少移除一個或者多移除一個都會造成程序崩潰:
3)嚴重依賴于string
KVO嚴重依賴string,換句話說,KVO中的keyPath必須是NSString這個事實使得編譯器沒辦法在編譯階段將錯誤的keyPath給找出來;譬如很容易將「contentSize」寫成「content size」;
4)
KVO的實現
KVO的實現也依賴于Objective-C的Runtime。
簡單概述下KVO的實現:
當你觀察一個對象(稱該對象為「被觀察對象」)時,一個新的類會動態被創建。這個類繼承自「被觀察對象」所對應類的,并重寫該被觀察屬性的setter方法;針對setter方法的重寫無非是在賦值語句前后加上相應的通知;最后,把「被觀察對象」的isa指針(isa指針告訴Runtime系統這個對象的類是什么)指向這個新創建的中間類,對象就神奇變成了新創建類的實例。
根據文檔的描述,雖然被觀察對象的isa指針被修改了,但是調用其class方法得到的類信息仍然是它之前所繼承類的類信息,而不是這個新創建類的類信息。
補充:下面對isa指針和類方法class作以更多的說明。
isa指針和類方法class的返回值都是Class類型,如下:
@interfaceNSObject {
ClassisaOBJC_ISA_AVAILABILITY;
}
- (Class)class;
根據我的理解,一般情況下,isa指針和class方法返回值都是一樣的;但KVO底層實現時,動態創建的類只是重寫了被觀察屬性的setter方法,并未重寫類方法class,因此向被觀察者發送class消息實際上仍然調用的是被觀察者原先類的類方法+ (Class)class,得到的類型信息當然是原先類的類信息,根據我的猜測,isKindOfClass:和isMemberOfClass:與class方法緊密相關。
國外的大神Mike Ash早在2009年就做了關于KVO的實現細節的探究,更多詳細參考這里。
下面來對這兩個參數進行詳細介紹。
options
options可選值是一個NSKeyValueObservingOptions枚舉值,到目前為止,一共包括四個值,在介紹這四個值各自表示的意思之前,先得有一個概念,即KVO響應方法有一個NSDictionary類型參數change(下面『響應』中可以看到),這個字典中會有一個與被監聽屬性相關的值,譬如被改變之前的值、新值等,NSDictionary中有啥值由『訂閱』時的options值決定,options可取值如下:
NSKeyValueObservingOptionNew: 指示change字典中包含新屬性值;
NSKeyValueObservingOptionOld: 指示change字典中包含舊屬性值;
NSKeyValueObservingOptionInitial: 相對復雜一些,NSKeyValueObserving.h文件中有詳細說明,此處略過;
NSKeyValueObservingOptionPrior: 相對復雜一些,NSKeyValueObserving.h文件中有詳細說明,此處略過;
現在細想,options這個參數也忒復雜了,難怪大神們覺得這個API丑陋(不過我等小民之前從未想過這個問題,=_=,沒辦法,Apple是個大帝國,我只是其中一個跪舔的小屁民)。
不過更糟心的是下面的context參數。
context
options信息量稍大,但其實蠻好理解的,然而對于context,在寫這篇博客之前,一直不知道context參數有啥用(也沒在意)。
context作用大了去了,在上文『KVO的槽點』提到一個槽點『多次相同的removeObserver會導致crash』。導致『多次調用相同的removeObserver』一個很重要的原因是我們經常在addObserver時為context參數賦值NULL,關于如何使用context參數,下面的『響應』中會提到。
響應
iOS的UI交互(譬如UIButton的一次點擊)有一個非常不錯的消息轉發機制 — Target-Action模型,簡單來說,為指定的event指定target和action處理方法。
UIButtonbutton = [UIButtonnew];
[button addTarget:selfaction:@selector(buttonDidClicked:) forControlEvents:UIControlEventTouchUpInside];
這種target-action模型邏輯非常清晰。作為對比,KVO的響應處理就非常糟糕了,所有的響應都對應是同一個方法- (void)observeValueForKeyPath:ofObject:change:context:,其原型如下:
-(void)observeValueForKeyPath:(NSString)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void *)context;
除了NSDictionary類型參數change之外,其余幾個參數都能在–addObserver:forKeyPath:options:context:找到對應。
下面將針對「嚴重依賴于string」和「多次相同的removeObserver會導致crash」這兩個槽點對keyPath和context參數進行闡述。
keyPath
keyPath的類型是NSString,這導致了我們使用了錯誤的keyPath而不自知,譬如將@”contentSize”錯誤寫成@”contentsize”,一個更好的方法是不直接使用@”xxxoo”,而是積極使用NSStringFromSelector(SEL aSelector)方法,即改@"contentSize"為NSStringFromSelector(@selector(contentSize))。
context
對于context,上文已經提到一種場景:假如父類(設為ClassA)和子類(設為ClassB)都監聽了同一個對象腫么辦?是ClassB處理呢還是交給父類ClassA的observeValueForKeyPath:ofObject:change:context:處理呢?更復雜一點,如果子類的子類(設為ClassC)也監聽了同一個對象,當ClassB接收到ClassC的[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];消息時又該如何處理呢?
用context參數判斷!
在addObserver時為context參數設置一個獨一無二的值即可,在responding處理時對這個context值進行檢驗。如此就解決了問題,但這需要靠用戶(各個層級類的程序員用戶)自覺遵守。