1. 程式人生 > IOS開發 >設計模式之(九)觀察者模式

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

本文首發於個人部落格

前言

什麼是觀察者模式

觀察者模式 屬於行為型模式。

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

模式結構

角色

  • 抽象主題(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中,寫如下程式碼

- (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"];
}

複製程式碼

執行之後結果為

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看這篇就夠了