iOS設計模式之觀察者模式

一水流年發表於2014-01-13

什麼是觀察者模式?我們先打個比方,這就像你訂報紙。比如你想知道美國最近放生了些新聞,你可能會訂閱一份美國週刊,然後一旦美國有了新的故事,美國週刊就發一刊,並郵寄給你,當你收到這份報刊,然後你就能夠了解美國最新的動態。其實這就是觀察者模式,A對B的變化感興趣,就註冊為B的觀察者,當B發生變化時通知A,告知B發生了變化。這是一種非常典型的觀察者的用法,我把這種使用方法叫做經典觀察者模式。當然與之相對的還有另外一種觀察者模式——廣義觀察者模式。

從經典的角度看,觀察者模式是一種通知變化的模式,一般認為只在物件發生變化感興趣的場合有用。主題物件知道有觀察者存在,設定會維護觀察者的一個佇列;而從廣義的角度看,觀察者模式是中傳遞變化資料的模式,需要檢視物件屬性時就會使用的一種模式,主題物件不知道觀察者的存在,更像是圍觀者。需要知道主題物件的狀態,所以即使在主題物件沒有發生改變的時候,觀察者也可能會去訪問主題物件。換句話說廣義觀察者模式,是在不同的物件之間傳遞資料的一種模式。

觀察者模式應當是在物件導向程式設計中被大規模使用的設計模式之一。從方法論的角度出發,傳統的認知論認為,世界是由物件組成的,我們通過不停的觀察和了解就能夠了解物件的本質。整個人類的認知模型就是建立在“觀察”這種行為之上的。我們通過不停與世界中的其他物件互動,並觀察之來了解這個世界。同樣,在程式的世界中,我們構建的每一個例項,也是通過不不停的與其他物件互動(檢視其他物件的狀態,或者改變其他物件的狀態),並通過觀察其他例項的變化並作出響應,以來完成功能。這也就是,為什麼會把觀察模式單獨提出來,做一個專門的剖析的原因——在我看來他是很多其他設計模式的基礎模式,並且是程式設計中極其重要的一種設計模式。


經典觀察者模式

經典觀察者模式被認為是物件的行為模式,又叫釋出-訂閱(Publish/Subscribe)模式、模型-檢視(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。經典觀察者模式定義了一種一對多的依賴關係,讓多個觀察者物件同時監聽某一個主題物件。這個主題物件在狀態上發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己或者做出相應的一些動作。在文章一開始舉的例子就是典型觀察者模式的應用。

而在IOS開發中我們可能會接觸到的經典觀察者模式的實現方式,有這麼幾種:NSNotificationCenter、KVO、Delegate等


感知通知方式

在經典觀察者模式中,因為觀察者感知到主題物件變化方式的不同,又分為推模型和拉模型兩種方式。

推模型

ios desing pattern observer 1

主題物件向觀察者推送主題的詳細資訊,不管觀察者是否需要,推送的資訊通常是主題物件的全部或者部分資料。推模型實現了觀察者和主題物件的解耦,兩者之間沒有過度的依賴關係。但是推模型每次都會以廣播的方式,向所有觀察者傳送通知。所有觀察者被動的接受通知。當通知的內容過多時,多個觀察者同時接收,可能會對網路、記憶體(有些時候還會涉及IO)有較大影響。

在IOS中典型的推模型實現方式為NSNotificationCenter和KVO。

NSNotificationCenter

NSnotificationCenter是一種典型的有排程中心的觀察者模式實現方式。以NSNotificationCenter為中心,觀察者往Center中註冊對某個主題物件的變化感興趣,主題物件通過NSNotificationCenter進行變化廣播。這種模型就是文章開始釋出訂閱報紙在OC中的一種類似實現。所有的觀察和監聽行為都向同一個中心註冊,所有物件的變化也都通過同一個中心向外廣播。

SNotificationCenter就像一個樞紐一樣,處在整個觀察者模式的核心位置,排程著訊息在觀察者和監聽者之間傳遞。

ios desing pattern observer 2

一次完整的觀察過程如上圖所示。整個過程中,關鍵的類有這麼幾個(介紹順序按照完成順序):

  1. 觀察者Observer,一般繼承自NSObject,通過NSNotificationCenter的addObserver:selector:name:object介面來註冊對某一型別通知感興趣.在註冊時候一定要注意,NSNotificationCenter不會對觀察者進行引用計數+1的操作,我們在程式中釋放觀察者的時候,一定要去報從center中將其登出了。
  2. 通知中心NSNotificationCenter,通知的樞紐。
  3. 主題物件,被觀察的物件,通過postNotificationName:object:userInfo:傳送某一型別通知,廣播改變。
  4. 通知物件NSNotification,當有通知來的時候,Center會呼叫觀察者註冊的介面來廣播通知,同時傳遞儲存著更改內容的NSNotification物件。

apple版實現的NotificationCenter讓我用起來不太爽的幾個小問題

在使用NSNotificationCenter的時候,從程式設計的角度來講我們往往不止是希望能夠做到功能實現,還能希望編碼效率和整個工程的可維護性良好。而Apple提供的以NSNotificationCenter為中心的觀察者模式實現,在可維護性和效率上存在以下缺點:

  1. 每個註冊的地方需要同時註冊一個函式,這將會帶來大量的編碼工作。仔細分析能夠發現,其實我們每個觀察者每次註冊的函式幾乎都是雷同的。這就是種變相的CtrlCV,是典型的醜陋和難維護的程式碼。
  2. 每個觀察者的回撥函式,都需要對主題物件傳送來的訊息進行解包的操作。從UserInfo中通過KeyValue的方式,將訊息解析出來,而後進行操作。試想一下,工程中有100個地方,同時對前面中在響應變化的函式中進行了解包的操作。而後期需求變化需要多傳一個內容的時候,將會是一場維護上的災難。
  3. 當大規模使用觀察者模式的時候,我們往往在dealloc處加上一句:
    [[NSNotificationCenter defaultCenter] removeObserver:self]
    而在實際使用過程中,會發現該函式的效能是比較低下的。在整個啟動過程中,進行了10000次RemoveObserver操作,


    通過下圖可以看出這一過程消耗了23.4%的CPU,說明這一函式的效率還是很低的。
    ios desing pattern observer 6
    這還是隻有一種訊息型別的存在下有這樣的結果,如果整個NotificationCenter中混雜著多種訊息型別,那麼恐怕對於效能來說將會是災難性的。

    增加了多種訊息型別之後,RemoveObserver佔用了啟動過程中63.9%的CPU消耗。
    ios desing pattern observer 7
    而由於Apple沒有提供Center的原始碼,所以修改這個Center幾乎不可能了。
改進版的有中心觀察者模式(DZNotificationCenter)

GitHub地址 在設計的時候考慮到以上用起來不爽的地方,進行了優化:

  1. 將解包到執行函式的操作進行了封裝,只需要提供某訊息型別的解包block和訊息型別對應的protocol,當有訊息到達的時候,訊息中心會進行統一解包,並直接呼叫觀察者相應的函式。
  2. 對觀察者的維護機制進行優化(還未做完),提升查詢和刪除觀察者的效率。

DZNotificationCenter的用法和NSNotificationCenter在註冊和登出觀察者的地方是一樣的,不一樣的地方在於,你在使用的時候需要提供解析訊息的block。你可以通過兩種方式來提供。

  • 直接註冊的方式
  • 實現DZNotificationInitDelegaete協議,當整個工程中大規模使用觀察者的時候,建議使用該方式。這樣有利於統一管理所有的解析方式。

在使用的過程中為了,能夠保證在觀察者處能夠回撥相同的函式,可以實現針對某一訊息型別的protocol

這樣就能夠保證,在使用觀察者的地方不用反覆的拼函式名和解析訊息內容了。

KVO

KVO的全稱是Key-Value Observer,即鍵值觀察。是一種沒有中心樞紐的觀察者模式的實現方式。一個主題物件管理所有依賴於它的觀察者物件,並且在自身狀態發生改變的時候主動通知觀察者物件。 讓我們先看一個完整的示例:

完成一次完整的改變通知過程,經過以下幾次過程:

  1. 註冊觀察者[message addObserver:self forKeyPath:kKVOPathKey options:NSKeyValueObservingOptionNew context:Nil];
  2. 更改主題物件屬性的值,即觸發傳送更改的通知 _message.key = @"asdfasd";
  3. 在制定的回撥函式中,處理收到的更改通知
  4. 登出觀察者 [_message removeObserver:self forKeyPath:kKVOPathKey];

KVO實現原理

一般情況下對於使用Property的屬性,objc會為其自動新增鍵值觀察功能,你只需要寫一句@property (noatomic, assign) float age 就能夠獲得age的鍵值觀察功能。而為了更深入的探討一下,KVO的實現原理我們先手動實現一下KVO:

首先,需要手動實現屬性的 setter 方法,並在設定操作的前後分別呼叫 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用於通知系統該 key 的屬性值即將和已經變更了;

其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設定對該 key 不自動傳送通知(返回 NO 即可)。這裡要注意,對其它非手動實現的 key,要轉交給 super 來處理。

在這裡的手動實現,主要是手動實現了主題物件變更向外廣播的過程。後續如何廣播到觀察者和觀察者如何響應我們沒有實現,其實這兩個過程apple已經封裝的很好了,猜測一下的話,應該是主題物件會維護一個觀察者的佇列,當本身屬性發生變動,接受到通知的時候,找到相關屬性的觀察者佇列,依次呼叫observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context來廣播更改。 還有一個疑問,就是在自動實現KVO的時候,系統是否和我們手動實現做了同樣的事情呢?

自動實現KVO及其原理

我們仔細來觀察一下在使用KVO的過程中類DZMessage的一個例項發生了什麼變化: 在使用KVO之前:

ios desing pattern observer 3

當呼叫Setter方法,並打了斷點的時候:

ios desing pattern observer 4

神奇的發現類的isa指標發生了變化,我們原本的類叫做DZMessage,而使用KVO後類名變成了NSKVONotifying_DZMessage。這說明objc在執行時對我們的類做了些什麼。

我們從Apple的文件Key-Value Observing Implementation Details找到了一些線索。

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.

當某一個類的例項第一次使用KVO的時候,系統就會在執行期間動態的建立該類的一個派生類,該類的命名規則一般是以NSKVONotifying為字首,以原本的類名為字尾。並且將原型的物件的isa指標指向該派生類。同時在派生類中過載了使用KVO的屬性的setter方法,在過載的setter方法中實現真正的通知機制,正如前面我們手動實現KVO一樣。這麼做是基於設定屬性會呼叫 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設定方式來變更屬性值,如果僅是直接修改屬性對應的成員變數,是無法實現 KVO 的。

同時派生類還重寫了 class 方法以“欺騙”外部呼叫者它就是起初的那個類。因此這個物件就成為該派生類的物件了,因而在該物件上對 setter 的呼叫就會呼叫重寫的 setter,從而啟用鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。


拉模型

ios desing pattern observer 5

拉模型是指主題物件在通知觀察者的時候,只傳遞少量資訊或者只是通知變化。如果觀察者需求要更具體的資訊,由觀察者主動從主題物件中拉取資料。相比推模型來說,拉模型更加自由,觀察者只要知道有情況發生就好了,至於什麼時候獲取、獲取那些內容、甚至是否獲取都可以自主決定。但是,卻存在兩個問題:

  • 如果某個觀察者響應過慢,可能會漏掉之前通知的內容
  • 觀察者必須儲存一個對目標物件的引用,而且還需要了解主題物件的結構,這就使觀察者產生了對主題物件的依賴。

可能每種設計模式都會存在或多或少的一些弊端,但是他們的確能夠解決問題,也有更多有用的地方。在使用的時候,就需要我們權衡利弊,做出一個合適的選擇。而工程師的價值就體現在,能夠在紛繁複雜的工具世界中找到最有效的那個。而如果核桃沒被砸開,不是你手力氣不大的問題,而是你選錯了工具,誰讓你非得用門縫夾,不用錘子呢!

當然,上面那段屬於題外話。言歸正傳,在OBJC程式設計中,典型的一種拉模型的實現是delegate。可能很多人會不同意我的觀點,說delegate應當是委託模式。好吧,我不否認,delegate的確是委託模式的一種極度典型的實現方式。但是這並不妨礙,他也是一種觀察者模式。其實本來各種設計模式之間就不是涇渭分明的。在使用和解釋的時候,只要你能夠說得通,而且能夠解決問題就好了,沒必要糾纏他們的名字。而在通知變化這個事情上delegate的確是能夠解決問題的。

我們來看一個使用delegate實現拉模型的觀察者的例子:

  • 先實現一個delegate方便註冊觀察者,和回撥函式

  • 註冊觀察者

  • 當主題物件的屬性發生改變的時候,傳送內容有變化的通知

  • 觀察者收到主題物件有變化的通知後,主動去拉取變化的內容。


廣義觀察者模式

在上面介紹了,觀察者被動的接受主題改變的經典意義上的觀察者模式之後,我們再來看一下廣義觀察者模式。當然上面所講的經典觀察者模式,也是一種一種傳遞資料的方式。廣義觀察者涵蓋了經典觀察者模式。

往往我們會有需要在“觀察者”和“主題物件”之間傳遞變化的資料。而這種情況下,主題物件可能不會像經典觀察者模式中的主題物件那樣勤勞,在發生改變的時候不停的廣播。在廣義觀察者模式中,主題物件可能是懶惰的,而是由觀察者通過不停的查詢主題物件的狀態,來獲知改變的內容。

我們熟悉的伺服器CS架構,始終比較典型的冠以觀察者模式,伺服器是伺服的,等待著客戶端的訪問,客戶端通過訪問伺服器來獲取最新的內容,而不是伺服器主動的推送。

之所以,要提出廣義觀察者模式這樣一個概念。是為了探討一下觀察者模式的本質。方便我們能夠更深刻的理解觀察者模式,並且合理的使用它。而且我們平時更多的將注意力放在了通知變化上面,而觀察者根本的目的是在於,在觀察者和主題物件之間,傳遞變化的資料。這些資料可能是變化這個事件本身,也可能是變化的內容,甚至可能是一些其他的內容。

從變化資料傳遞的角度來思考的話,能夠實現這個的模式和策略實在是數不勝數,比如傳統的網路CS模型,比如KVC等等。在這裡就先不詳細展開討論了。

參考:

  1. http://www.cnblogs.com/java-my-life/archive/2012/05/16/2502279.html
  2. http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html
  3. 《模式工程化實現及擴充套件》

相關文章