什麼是觀察者模式?我們先打個比方,這就像你訂報紙。比如你想知道美國最近放生了些新聞,你可能會訂閱一份美國周刊,然後一旦美國有了新的故事,美國周刊就發一刊,並郵寄給你,當你收到這份報刊,然後你就能夠了解美國最新的動態。其實這就是觀察者模式,A對B的變化感興趣,就注冊為B的觀察者,當B發生變化時通知A,告知B發生了變化。這是一種非常典型的觀察者的用法,我把這種使用方法叫做經典觀察者模式。當然與之相對的還有另外一種觀察者模式——廣義觀察者模式。
從經典的角度看,觀察者模式是一種通知變化的模式,一般認為只在對象發生變化感興趣的場合有用。主題對象知道有觀察者存在,設置會維護觀察者的一個隊列;而從廣義的角度看,觀察者模式是中傳遞變化數據的模式,需要查看對象屬性時就會使用的一種模式,主題對象不知道觀察者的存在,更像是圍觀者。需要知道主題對象的狀態,所以即使在主題對象沒有發生改變的時候,觀察者也可能會去訪問主題對象。換句話說廣義觀察者模式,是在不同的對象之間傳遞數據的一種模式。
觀察者模式應當是在面向對象編程中被大規模使用的設計模式之一。從方法論的角度出發,傳統的認知論認為,世界是由對象組成的,我們通過不停的觀察和了解就能夠了解對象的本質。整個人類的認知模型就是建立在“觀察”這種行為之上的。我們通過不停與世界中的其他對象交互,並觀察之來了解這個世界。同樣,在程序的世界中,我們構建的每一個實例,也是通過不不停的與其他對象交互(查看其他對象的狀態,或者改變其他對象的狀態),並通過觀察其他實例的變化並作出響應,以來完成功能。這也就是,為什麼會把觀察模式單獨提出來,做一個專門的剖析的原因——在我看來他是很多其他設計模式的基礎模式,並且是編程中極其重要的一種設計模式。
經典觀察者模式
經典觀察者模式被認為是對象的行為模式,又叫發布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。經典觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己或者做出相應的一些動作。在文章一開始舉的例子就是典型觀察者模式的應用。
而在IOS開發中我們可能會接觸到的經典觀察者模式的實現方式,有這麼幾種:NSNotificationCenter、KVO、Delegate等
感知通知方式
在經典觀察者模式中,因為觀察者感知到主題對象變化方式的不同,又分為推模型和拉模型兩種方式。
推模型
主題對象向觀察者推送主題的詳細信息,不管觀察者是否需要,推送的信息通常是主題對象的全部或者部分數據。推模型實現了觀察者和主題對象的解耦,兩者之間沒有過度的依賴關系。但是推模型每次都會以廣播的方式,向所有觀察者發送通知。所有觀察者被動的接受通知。當通知的內容過多時,多個觀察者同時接收,可能會對網絡、內存(有些時候還會涉及IO)有較大影響。
在IOS中典型的推模型實現方式為NSNotificationCenter和KVO。
NSNotificationCenter
NSnotificationCenter是一種典型的有調度中心的觀察者模式實現方式。以NSNotificationCenter為中心,觀察者往Center中注冊對某個主題對象的變化感興趣,主題對象通過NSNotificationCenter進行變化廣播。這種模型就是文章開始發布訂閱報紙在OC中的一種類似實現。所有的觀察和監聽行為都向同一個中心注冊,所有對象的變化也都通過同一個中心向外廣播。
SNotificationCenter就像一個樞紐一樣,處在整個觀察者模式的核心位置,調度著消息在觀察者和監聽者之間傳遞。
一次完整的觀察過程如上圖所示。整個過程中,關鍵的類有這麼幾個(介紹順序按照完成順序):
觀察者Observer,一般繼承自NSObject,通過NSNotificationCenter的addObserver:selector:name:object接口來注冊對某一類型通知感興趣.在注冊時候一定要注意,NSNotificationCenter不會對觀察者進行引用計數+1的操作,我們在程序中釋放觀察者的時候,一定要去報從center中將其注銷了。
- (void) handleMessage:(NSNotification*)nc{ //解析消息內容 NSDictionary* userInfo = [nc userInfo]; } - (void) commonInit { //注冊觀察者 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMessage:) name:kDZTestNotificatonMessage object:nil]; }
通知中心NSNotificationCenter,通知的樞紐。
主題對象,被觀察的對象,通過postNotificationName:object:userInfo:發送某一類型通知,廣播改變。
- (void) postMessage { [[NSNotificationCenter defaultCenter] postNotificationName:kDZTestNotificatonMessage object:Nil userInfo:@{}]; }
通知對象NSNotification,當有通知來的時候,Center會調用觀察者注冊的接口來廣播通知,同時傳遞存儲著更改內容的NSNotification對象。
apple版實現的NotificationCenter讓我用起來不太爽的幾個小問題
在使用NSNotificationCenter的時候,從編程的角度來講我們往往不止是希望能夠做到功能實現,還能希望編碼效率和整個工程的可維護性良好。而Apple提供的以NSNotificationCenter為中心的觀察者模式實現,在可維護性和效率上存在以下缺點:
每個注冊的地方需要同時注冊一個函數,這將會帶來大量的編碼工作。仔細分析能夠發現,其實我們每個觀察者每次注冊的函數幾乎都是雷同的。這就是種變相的CtrlCV,是典型的丑陋和難維護的代碼。
每個觀察者的回調函數,都需要對主題對象發送來的消息進行解包的操作。從UserInfo中通過KeyValue的方式,將消息解析出來,而後進行操作。試想一下,工程中有100個地方,同時對前面中在響應變化的函數中進行了解包的操作。而後期需求變化需要多傳一個內容的時候,將會是一場維護上的災難。
當大規模使用觀察者模式的時候,我們往往在dealloc處加上一句:
[[NSNotificationCenter defaultCenter] removeObserver:self]
而在實際使用過程中,會發現該函數的性能是比較低下的。在整個啟動過程中,進行了10000次RemoveObserver操作,
@implementation DZMessage - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } .... for (int i = ; i < ; i++) { DZMessage* message = [DZMessage new]; }
通過下圖可以看出這一過程消耗了23.4%的CPU,說明這一函數的效率還是很低的。
這還是只有一種消息類型的存在下有這樣的結果,如果整個NotificationCenter中混雜著多種消息類型,那麼恐怕對於性能來說將會是災難性的。
for (int i = 0 ; i < 10000; i++) { DZMessage* message = [DZMessage new]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handle) name:[@(i) stringValue] object:nil]; }
增加了多種消息類型之後,RemoveObserver占用了啟動過程中63.9%的CPU消耗。
而由於Apple沒有提供Center的源碼,所以修改這個Center幾乎不可能了。
改進版的有中心觀察者模式(DZNotificationCenter)
GitHub地址 在設計的時候考慮到以上用起來不爽的地方,進行了優化:
將解包到執行函數的操作進行了封裝,只需要提供某消息類型的解包block和消息類型對應的protocol,當有消息到達的時候,消息中心會進行統一解包,並直接調用觀察者相應的函數。
對觀察者的維護機制進行優化(還未做完),提升查找和刪除觀察者的效率。
DZNotificationCenter的用法和NSNotificationCenter在注冊和注銷觀察者的地方是一樣的,不一樣的地方在於,你在使用的時候需要提供解析消息的block。你可以通過兩種方式來提供。
直接注冊的方式
[DZDefaultNotificationCenter addDecodeNotificationBlock:^SEL(NSDictionary *userInfo, NSMutableArray *__autoreleasing *params) { NSString* key = userInfo[@"key"]; if (params != NULL) { *params = [NSMutableArray new]; } [*params addObject:key]; return @selector(handleTestMessageWithKey:); } forMessage:kDZMessageTest];
實現DZNotificationInitDelegaete協議,當整個工程中大規模使用觀察者的時候,建議使用該方式。這樣有利於統一管理所有的解析方式。
- (DZDecodeNotificationBlock) decodeNotification:(NSString *)message forCenter:(DZNotificationCenter *)center { if (message == kDZMessageTest) { return ^(NSDictionary* userInfo, NSMutableArray* __autoreleasing* params){ NSString* key = userInfo[@"key"]; if (params != NULL) { *params = [NSMutableArray new]; } [*params addObject:key]; return @selector(handlePortMessage:); }; } return nil; }
在使用的過程中為了,能夠保證在觀察者處能夠回調相同的函數,可以實現針對某一消息類型的protocol
@protocol DZTestMessageInterface <NSObject> - (void) handleTestMessageWithKey:(NSString*)key; @end
這樣就能夠保證,在使用觀察者的地方不用反復的拼函數名和解析消息內容了。
@interface DZViewController () <DZTestMessageInterface> @end @implementation DZViewController .... - (void) handleTestMessageWithKey:(NSString *)key { self.showLabel.text = [NSString stringWithFormat:@"get message with %@", key]; } ....
KVO
KVO的全稱是Key-Value Observer,即鍵值觀察。是一種沒有中心樞紐的觀察者模式的實現方式。一個主題對象管理所有依賴於它的觀察者對象,並且在自身狀態發生改變的時候主動通知觀察者對象。 讓我們先看一個完整的示例:
static NSString* const kKVOPathKey = @"key"; @implementation DZKVOTest - (void) setMessage:(DZMessage *)message { if (message != _message) { if (_message) { [_message removeObserver:self forKeyPath:kKVOPathKey]; } if (message) { [message addObserver:self forKeyPath:kKVOPathKey options:NSKeyValueObservingOptionNew context:Nil]; } _message = message; } } - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqual:kKVOPathKey] && object == _message) { NSLog(@"get %@",change); } } - (void) postMessage { _message.key = @"asdfasd"; } @end
完成一次完整的改變通知過程,經過以下幾次過程:
注冊觀察者[message addObserver:self forKeyPath:kKVOPathKey options:NSKeyValueObservingOptionNew context:Nil];
更改主題對象屬性的值,即觸發發送更改的通知 _message.key = @"asdfasd";
在制定的回調函數中,處理收到的更改通知
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqual:kKVOPathKey] && object == _message) { NSLog(@"get %@",change); } }
注銷觀察者 [_message removeObserver:self forKeyPath:kKVOPathKey];
KVO實現原理
一般情況下對於使用Property的屬性,objc會為其自動添加鍵值觀察功能,你只需要寫一句@property (noatomic, assign) float age 就能夠獲得age的鍵值觀察功能。而為了更深入的探討一下,KVO的實現原理我們先手動實現一下KVO:
@implementation DZKVOManual - (void) setAge:(int)age { [self willChangeValueForKey:kKVOPathAge]; if (age !=_age) { _age = age; } [self didChangeValueForKey:kKVOPathAge]; } //經驗證 會先去調用automaticallyNotifiesObserversForKey:當該函數沒有時才會調用automaticallyNotifiesObserversOfAge。這個函數應該是編譯器,自動增加的一個函數,使用xcode能夠自動提示出來。的確很強大。 //+(BOOL) automaticallyNotifiesObserversOfAge //{ // return NO; //} + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key { if (key ==kKVOPathAge) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; } @end
首先,需要手動實現屬性的 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之前:
當調用Setter方法,並打了斷點的時候:
神奇的發現類的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 方法來釋放資源。
拉模型
拉模型是指主題對象在通知觀察者的時候,只傳遞少量信息或者只是通知變化。如果觀察者需求要更具體的信息,由觀察者主動從主題對象中拉取數據。相比推模型來說,拉模型更加自由,觀察者只要知道有情況發生就好了,至於什麼時候獲取、獲取那些內容、甚至是否獲取都可以自主決定。但是,卻存在兩個問題:
如果某個觀察者響應過慢,可能會漏掉之前通知的內容
觀察者必須保存一個對目標對象的引用,而且還需要了解主題對象的結構,這就使觀察者產生了對主題對象的依賴。
可能每種設計模式都會存在或多或少的一些弊端,但是他們的確能夠解決問題,也有更多有用的地方。在使用的時候,就需要我們權衡利弊,做出一個合適的選擇。而工程師的價值就體現在,能夠在紛繁復雜的工具世界中找到最有效的那個。而如果核桃沒被砸開,不是你手力氣不大的問題,而是你選錯了工具,誰讓你非得用門縫夾,不用錘子呢!
當然,上面那段屬於題外話。言歸正傳,在OBJC編程中,典型的一種拉模型的實現是delegate。可能很多人會不同意我的觀點,說delegate應當是委托模式。好吧,我不否認,delegate的確是委托模式的一種極度典型的實現方式。但是這並不妨礙,他也是一種觀察者模式。其實本來各種設計模式之間就不是泾渭分明的。在使用和解釋的時候,只要你能夠說得通,而且能夠解決問題就好了,沒必要糾纏他們的名字。而在通知變化這個事情上delegate的確是能夠解決問題的。
我們來看一個使用delegate實現拉模型的觀察者的例子:
先實現一個delegate方便注冊觀察者,和回調函數
@class DZClient; @protocol DZClientChangedDelegate <NSObject> - (void) client:(DZClient*)client didChangedContent:(NSString*)key; @end @interface DZClient : NSObject @property (nonatomic, weak) id<DZClientChangedDelegate> delegate; @property (nonatomic, strong) NSString* key; @end
注冊觀察者
//DZAppDelegate DZClient* client = [DZClient new]; client.delegate = self; client.key = @"aa";
當主題對象的屬性發生改變的時候,發送內容有變化的通知
@implementation DZClient - (void) setKey:(NSString *)key { if (_key != key) { _key = key; if ([_delegate respondsToSelector:@selector(client:didChangedContent:)]) { [_delegate client:self didChangedContent:@"key"]; } } }
觀察者收到主題對象有變化的通知後,主動去拉取變化的內容。
//DZAppDelegate - (void) client:(DZClient *)client didChangedContent:(NSString *)key { if ([key isEqual: @"key"]) { NSLog(@"get changed key %@",client.key); } }
廣義觀察者模式
在上面介紹了,觀察者被動的接受主題改變的經典意義上的觀察者模式之後,我們再來看一下廣義觀察者模式。當然上面所講的經典觀察者模式,也是一種一種傳遞數據的方式。廣義觀察者涵蓋了經典觀察者模式。
往往我們會有需要在“觀察者”和“主題對象”之間傳遞變化的數據。而這種情況下,主題對象可能不會像經典觀察者模式中的主題對象那樣勤勞,在發生改變的時候不停的廣播。在廣義觀察者模式中,主題對象可能是懶惰的,而是由觀察者通過不停的查詢主題對象的狀態,來獲知改變的內容。
我們熟悉的服務器CS架構,始終比較典型的冠以觀察者模式,服務器是伺服的,等待著客戶端的訪問,客戶端通過訪問服務器來獲取最新的內容,而不是服務器主動的推送。
之所以,要提出廣義觀察者模式這樣一個概念。是為了探討一下觀察者模式的本質。方便我們能夠更深刻的理解觀察者模式,並且合理的使用它。而且我們平時更多的將注意力放在了通知變化上面,而觀察者根本的目的是在於,在觀察者和主題對象之間,傳遞變化的數據。這些數據可能是變化這個事件本身,也可能是變化的內容,甚至可能是一些其他的內容。
從變化數據傳遞的角度來思考的話,能夠實現這個的模式和策略實在是數不勝數,比如傳統的網絡CS模型,比如KVC等等。在這裡就先不詳細展開討論了。
以上所述是本文給大家介紹的ios觀察者設計模式,希望大家喜歡。