設計模式之(九)觀察者模式

本文首發於我的博客html

前言

什麼是觀察者模式

觀察者模式 屬於行爲型模式。git

觀察者模式(有時又被稱爲模型(Model)-視圖(View)模式、源-收聽者(Listener)模式或從屬者模式)是軟件設計模式的一種。在此種模式中,一個目標物件管理全部相依於它的觀察者物件,而且在它自己的狀態改變時主動發出通知。這一般透過呼叫各觀察者所提供的方法來實現。此種模式一般被用來實現事件處理系統。github

模式結構

角色

  • 抽象主題(Subject): 它把全部觀察者對象的引用保存到一個彙集裏,每一個主題均可以有任何數量的觀察者。抽象主題提供一個接口,能夠增長和刪除觀察者對象。
  • 具體主題(Concrete Subject): 將有關狀態存入具體觀察者對象;在具體主題內部狀態改變時,給全部登記過的觀察者發出通知。
  • 抽象觀察者(Observer): 爲全部的具體觀察者定義一個接口,在獲得主題通知時更新本身。
  • 具體觀察者(Concrete Observer): 實現抽象觀察者角色所要求的更新接口,以便使自己的狀態與主題狀態協調

使用場景:

  • 當一個抽象模型有兩個方面,其中一個方面依賴於另外一方面。將這兩者封裝在獨立的對象中以使它們能夠各自獨立地改變和複用。
  • 當對一個對象的改變須要同時改變其餘對象,而不知道具體有多少對象須要被改變。
  • 當一個對象必須通知其餘對象,而它又不能假定其餘對象是誰。換言之,不但願這些對象是緊密耦合的

優缺點

  • 觀察者模式的主要的做用就是對對象解耦,將觀察者和被觀察者徹底隔離。

觀察者模式的優勢

  • 觀察者模式解除了主題和具體觀察者的耦合,讓耦合的雙方都依賴於抽象,而不是依賴具體。

觀察者模式的缺點

  • 在應用觀察者模式時須要考慮一下開發小路問題,程序中包括一個被觀察者和多個被觀察者,開發和調試比較複雜,並且Java中的消息的通知默認是順序執行的,一個觀察者的卡頓會影響總體的執行效率。在這種狀況下,通常考慮採用異步的方式。

iOS中的觀察者模式

通常兩種:KVO和通知。通知比較簡單,這裏只說一下KVO編程

  • KVO全稱KeyValueObserving,俗稱鍵值監聽,是蘋果提供的一套事件通知機制。容許對象監聽另外一個對象特定屬性的改變,並在改變時接收到事件。因爲KVO的實現機制,因此對屬性纔會發生做用,通常繼承自NSObject的對象都默認支持KVO。
  • KVC和KVO都屬於鍵值編程並且底層實現機制都是isa-swizzing
  • KVO和NSNotificationCenter都是iOS中觀察者模式的一種實現。KVO對被監聽對象無侵入性,不須要修改其內部代碼便可實現監聽。
  • KVO能夠監聽單個屬性的變化,也能夠監聽集合對象的變化。經過KVC的mutableArrayValueForKey:等方法得到代理對象,當代理對象的內部對象發生改變時,會回調KVO監聽的方法。集合對象包含NSArray和NSSet。

實現原理

  • KVO是經過isa-swizzling技術實現的(這句話是整個KVO實現的重點)。
  • 在運行時根據原類建立一箇中間類,這個中間類是原類的子類,並動態修改當前對象的isa指向中間類。當修改 instance 對象的屬性時,會調用 Foundation框架的 _NSSetXXXValueAndNotify 函數 ,該函數裏面會先調用 willChangeValueForKey: 而後調用父類原來的 setter 方法修改值,最後是 didChangeValueForKey:。didChangeValueForKey 內部會觸發監聽器(Oberser)的監聽方法observeValueForKeyPath:ofObject:change:context:
  • 而且將class方法重寫,返回原類的Class。

KVO的使用

使用方法

  1. 經過addObserver:forKeyPath:options:context:方法註冊觀察者,觀察者能夠接收keyPath屬性的變化事件。
  2. 在觀察者中實現observeValueForKeyPath:ofObject:change:context:方法,當keyPath屬性發生改變後,KVO會回調這個方法來通知觀察者。
  3. 當觀察者不須要監聽時,能夠調用removeObserver:forKeyPath:方法將KVO移除。須要注意的是,調用removeObserver須要在觀察者消失以前,不然會致使Crash。

例如,咱們定義一個 YZPerson 類 繼承自 NSObject ,裏面有name 和 age 兩個屬性設計模式

@interface YZPerson : NSObject
@property (nonatomic ,assign) int age;
@property (nonatomic,strong) NSString  *name;
@end

複製代碼

而後在ViewController中,寫以下代碼bash

- (void)viewDidLoad {
    [super viewDidLoad];
   	//調用方法
    [self setNameKVO];
}

-(void)setNameKVO{
    self.person = [[YZPerson alloc] init];
    // 註冊觀察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
 
}

// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  self.person.name = @"ccc";

}

-(void)dealloc
{
    // 移除監聽
    [self.person removeObserver:self forKeyPath:@"name"];
}

複製代碼

執行以後結果爲app

KVOdemo[11482:141804] 監聽到<YZPerson: 0x6000004e8400>的name屬性值改變了 - {
    kind = 1;
    new = ccc;
    old = "<null>";
} - 1111- 1111
複製代碼

注意點

須要注意的是,上面代碼中咱們已經移除了監聽,若是再次移除的話,就會crash框架

例如異步

- (void)viewDidLoad {
    [super viewDidLoad];
   	//調用方法
    [self setNameKVO];
}
-(void)setNameKVO{
   self.person = [[YZPerson alloc] init];
    // 註冊觀察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
       // 移除監聽
    [person removeObserver:self forKeyPath:@"name"];
    // 再次移除
     [person removeObserver:self forKeyPath:@"name"];

}
複製代碼

移除屢次會報錯函數

KVOdemo[9261:2171323] *** Terminating app due to uncaught exception 'NSRangeException', 
reason: 'Cannot remove an observer <ViewController 0x139e07220> for the key path "name" from <YZPerson 0x281322f20> because it is not registered as an observer.'
複製代碼

若是忘記移除的話,有可能下次收到這個屬性的變化的時候,會carsh

因此,咱們要保證add和remove是成對出現的

資料

更多關於KVO的內容,包括KVO的本質,KVO內部的流程,手動調用KVO等,能夠參考以前的一篇文章關於KVO看這篇就夠了

相關文章
相關標籤/搜索