1.什么是觀察者模式?
2.為什么要用觀察者模式?它的優(yōu)缺點(diǎn)是什么?
![Uploading 屏幕快照 2016-12-20 下午3.23.56_660666.png . . .]
3.觀察者模式解決了什么問題?你的項(xiàng)目中哪里用到了觀察者模式?
4.觀察者模式怎么用?有幾種方式?
5.怎么創(chuàng)建觀察者模式?
7.寫觀察者模式的時(shí)候需要注意什么問題?
8.iOS源代碼中哪里用到了觀察者模式?舉例說明。
9.實(shí)現(xiàn)原理,
根據(jù)上面的套路,我們一一回答問題。
1.什么是觀察者模式
KVO 是 Objective-C 對(duì)觀察者設(shè)計(jì)模式的一種實(shí)現(xiàn)。【另外一種是:通知機(jī)制(notification)
KVO提供一種機(jī)制,指定一個(gè)被觀察對(duì)象(例如A類),當(dāng)對(duì)象某個(gè)屬性(例如A中的字符串name)發(fā)生更改時(shí),對(duì)象會(huì)獲得通知,并作出相應(yīng)處理;【且不需要給被觀察的對(duì)象添加任何額外代碼,就能使用KVO機(jī)制】
在MVC設(shè)計(jì)架構(gòu)下的項(xiàng)目,KVO機(jī)制很適合實(shí)現(xiàn)mode模型和view視圖之間的通訊。
例如:代碼中,在模型類A創(chuàng)建屬性數(shù)據(jù),在控制器中創(chuàng)建觀察者,一旦屬性數(shù)據(jù)發(fā)生改變就收到觀察者收到通知,通過KVO再在控制器使用回調(diào)方法處理實(shí)現(xiàn)視圖B的更新;(本文中的應(yīng)用就是這樣的例子.)
簡單的說就是,當(dāng)某對(duì)象改變時(shí),自動(dòng)通知所有相關(guān)的狀態(tài)進(jìn)行更新。
2.為什么要用觀察者模式
觀察者模式定義了一種一對(duì)多的依賴關(guān)系,讓多個(gè)觀察者對(duì)象同時(shí)監(jiān)聽某一個(gè)主題對(duì)象。這個(gè)主題對(duì)象在狀態(tài)上發(fā)生變化時(shí),會(huì)通知所有觀察者對(duì)象,使它們能夠自動(dòng)更新自己。
觀察者模式的優(yōu)點(diǎn):
1、 Subject和Observer之間是松偶合的,分別可以各自獨(dú)立改變。
2、Subject在發(fā)送廣播通知的時(shí)候,無須指定具體的Observer,Observer可以自己決定是否要訂閱Subject的通知。
3、高內(nèi)聚、低偶合。
3.觀察者模式解決了什么問題?你的項(xiàng)目中哪里用到了觀察者模式?
有時(shí)候我們需要監(jiān)聽某個(gè)類的屬性值的變化從而做出相應(yīng)的改變,這個(gè)時(shí)候使用KVO/KVC設(shè)計(jì)模式。
比如,在項(xiàng)目中,我需要監(jiān)聽model中的某個(gè)屬性值的變換,當(dāng)變化時(shí),需要更新UI顯示。
//增加frame的監(jiān)聽
1)用于判斷當(dāng)前手勢滑動(dòng)的frame moveTableView:didChangeFromFrame:toFrame
2)增加搜索結(jié)果contentview 根據(jù)手勢切換導(dǎo)航顯示模式
[self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
4.觀察者模式怎么用?有幾種方式?
在iOS中觀察者模式的實(shí)現(xiàn)有四種方法:NSNotification、KVO、Protocol以及Code Block代碼塊。
要點(diǎn):
Notification是一對(duì)多的,而delegate回調(diào)是一對(duì)一的。
Notification - NotificationCenter機(jī)制使用了操作系統(tǒng)的對(duì)象間通訊功能,而delegate是直接的函數(shù)調(diào)用。Notification跨度大,而delegate效率可能比較高。
相較于前兩者KVO才是一種真正的觀察者模式,它允許你將一個(gè)處理函數(shù)綁定到某個(gè)類的屬性,屬性發(fā)生改變是就會(huì)自動(dòng)觸發(fā),不像其他兩種需要你手動(dòng)的發(fā)通知。KVO是一種非常靈活的觀察機(jī)制,廣泛應(yīng)用于界面設(shè)計(jì)。
Code Block其實(shí)就相當(dāng)于C的函數(shù)指針,可以用來做各種回調(diào)。我覺得其應(yīng)當(dāng)具備最高的效率。使用Code Block要注意的地方就是使用外部變量。在block里直接引用外部變量的話會(huì)在block定義的時(shí)候復(fù)制外部變量的一個(gè)拷貝,也就是說得到的是block定義時(shí)的值,在block內(nèi)修改這個(gè)值也不會(huì)傳給外部。要得到實(shí)時(shí)的數(shù)據(jù),或者將數(shù)據(jù)傳出的話需要在相關(guān)變量前面加__block即可。
NSNotification

發(fā)送通知:

實(shí)現(xiàn)通知的方法

KVO
//注冊(cè)通知
[_tableView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
//通知回調(diào)
- (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對(duì)象要通知B對(duì)象,B對(duì)象必須實(shí)現(xiàn)監(jiān)聽的方法,否則一旦有消息發(fā)送就會(huì)導(dǎo)致崩潰.
- A對(duì)象不想通知B對(duì)象了,需要從B對(duì)象身上移除掉通知.
但是要注意:添加的觀察者的次數(shù)要和移除觀察者的次數(shù)相等,少移除一個(gè)或者多移除一個(gè)都會(huì)造成程序崩潰:
3)嚴(yán)重依賴于string
KVO嚴(yán)重依賴string,換句話說,KVO中的keyPath必須是NSString這個(gè)事實(shí)使得編譯器沒辦法在編譯階段將錯(cuò)誤的keyPath給找出來;譬如很容易將「contentSize」寫成「content size」;
4)
KVO的實(shí)現(xiàn)
KVO的實(shí)現(xiàn)也依賴于Objective-C的Runtime。
簡單概述下KVO的實(shí)現(xiàn):
當(dāng)你觀察一個(gè)對(duì)象(稱該對(duì)象為「被觀察對(duì)象」)時(shí),一個(gè)新的類會(huì)動(dòng)態(tài)被創(chuàng)建。這個(gè)類繼承自「被觀察對(duì)象」所對(duì)應(yīng)類的,并重寫該被觀察屬性的setter方法;針對(duì)setter方法的重寫無非是在賦值語句前后加上相應(yīng)的通知;最后,把「被觀察對(duì)象」的isa指針(isa指針告訴Runtime系統(tǒng)這個(gè)對(duì)象的類是什么)指向這個(gè)新創(chuàng)建的中間類,對(duì)象就神奇變成了新創(chuàng)建類的實(shí)例。
根據(jù)文檔的描述,雖然被觀察對(duì)象的isa指針被修改了,但是調(diào)用其class方法得到的類信息仍然是它之前所繼承類的類信息,而不是這個(gè)新創(chuàng)建類的類信息。
補(bǔ)充:下面對(duì)isa指針和類方法class作以更多的說明。
isa指針和類方法class的返回值都是Class類型,如下:
@interfaceNSObject {
ClassisaOBJC_ISA_AVAILABILITY;
}
- (Class)class;
根據(jù)我的理解,一般情況下,isa指針和class方法返回值都是一樣的;但KVO底層實(shí)現(xiàn)時(shí),動(dòng)態(tài)創(chuàng)建的類只是重寫了被觀察屬性的setter方法,并未重寫類方法class,因此向被觀察者發(fā)送class消息實(shí)際上仍然調(diào)用的是被觀察者原先類的類方法+ (Class)class,得到的類型信息當(dāng)然是原先類的類信息,根據(jù)我的猜測,isKindOfClass:和isMemberOfClass:與class方法緊密相關(guān)。
國外的大神Mike Ash早在2009年就做了關(guān)于KVO的實(shí)現(xiàn)細(xì)節(jié)的探究,更多詳細(xì)參考這里。
下面來對(duì)這兩個(gè)參數(shù)進(jìn)行詳細(xì)介紹。
options
options可選值是一個(gè)NSKeyValueObservingOptions枚舉值,到目前為止,一共包括四個(gè)值,在介紹這四個(gè)值各自表示的意思之前,先得有一個(gè)概念,即KVO響應(yīng)方法有一個(gè)NSDictionary類型參數(shù)change(下面『響應(yīng)』中可以看到),這個(gè)字典中會(huì)有一個(gè)與被監(jiān)聽屬性相關(guān)的值,譬如被改變之前的值、新值等,NSDictionary中有啥值由『訂閱』時(shí)的options值決定,options可取值如下:
NSKeyValueObservingOptionNew: 指示change字典中包含新屬性值;
NSKeyValueObservingOptionOld: 指示change字典中包含舊屬性值;
NSKeyValueObservingOptionInitial: 相對(duì)復(fù)雜一些,NSKeyValueObserving.h文件中有詳細(xì)說明,此處略過;
NSKeyValueObservingOptionPrior: 相對(duì)復(fù)雜一些,NSKeyValueObserving.h文件中有詳細(xì)說明,此處略過;
現(xiàn)在細(xì)想,options這個(gè)參數(shù)也忒復(fù)雜了,難怪大神們覺得這個(gè)API丑陋(不過我等小民之前從未想過這個(gè)問題,=_=,沒辦法,Apple是個(gè)大帝國,我只是其中一個(gè)跪舔的小屁民)。
不過更糟心的是下面的context參數(shù)。
context
options信息量稍大,但其實(shí)蠻好理解的,然而對(duì)于context,在寫這篇博客之前,一直不知道context參數(shù)有啥用(也沒在意)。
context作用大了去了,在上文『KVO的槽點(diǎn)』提到一個(gè)槽點(diǎn)『多次相同的removeObserver會(huì)導(dǎo)致crash』。導(dǎo)致『多次調(diào)用相同的removeObserver』一個(gè)很重要的原因是我們經(jīng)常在addObserver時(shí)為context參數(shù)賦值NULL,關(guān)于如何使用context參數(shù),下面的『響應(yīng)』中會(huì)提到。
響應(yīng)
iOS的UI交互(譬如UIButton的一次點(diǎn)擊)有一個(gè)非常不錯(cuò)的消息轉(zhuǎn)發(fā)機(jī)制 — Target-Action模型,簡單來說,為指定的event指定target和action處理方法。
UIButtonbutton = [UIButtonnew];
[button addTarget:selfaction:@selector(buttonDidClicked:) forControlEvents:UIControlEventTouchUpInside];
這種target-action模型邏輯非常清晰。作為對(duì)比,KVO的響應(yīng)處理就非常糟糕了,所有的響應(yīng)都對(duì)應(yīng)是同一個(gè)方法- (void)observeValueForKeyPath:ofObject:change:context:,其原型如下:
-(void)observeValueForKeyPath:(NSString)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void *)context;
除了NSDictionary類型參數(shù)change之外,其余幾個(gè)參數(shù)都能在–addObserver:forKeyPath:options:context:找到對(duì)應(yīng)。
下面將針對(duì)「嚴(yán)重依賴于string」和「多次相同的removeObserver會(huì)導(dǎo)致crash」這兩個(gè)槽點(diǎn)對(duì)keyPath和context參數(shù)進(jìn)行闡述。
keyPath
keyPath的類型是NSString,這導(dǎo)致了我們使用了錯(cuò)誤的keyPath而不自知,譬如將@”contentSize”錯(cuò)誤寫成@”contentsize”,一個(gè)更好的方法是不直接使用@”xxxoo”,而是積極使用NSStringFromSelector(SEL aSelector)方法,即改@"contentSize"為NSStringFromSelector(@selector(contentSize))。
context
對(duì)于context,上文已經(jīng)提到一種場景:假如父類(設(shè)為ClassA)和子類(設(shè)為ClassB)都監(jiān)聽了同一個(gè)對(duì)象腫么辦?是ClassB處理呢還是交給父類ClassA的observeValueForKeyPath:ofObject:change:context:處理呢?更復(fù)雜一點(diǎn),如果子類的子類(設(shè)為ClassC)也監(jiān)聽了同一個(gè)對(duì)象,當(dāng)ClassB接收到ClassC的[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];消息時(shí)又該如何處理呢?
用context參數(shù)判斷!
在addObserver時(shí)為context參數(shù)設(shè)置一個(gè)獨(dú)一無二的值即可,在responding處理時(shí)對(duì)這個(gè)context值進(jìn)行檢驗(yàn)。如此就解決了問題,但這需要靠用戶(各個(gè)層級(jí)類的程序員用戶)自覺遵守。