iOS 觀察者KVO

KVO 的基本概念(Key Value Observing)
  • 基本概念
      鍵值觀察是一種使對(duì)象獲取其他對(duì)象的特定屬性變化的通知機(jī)制。控制器層的綁定技術(shù)就是嚴(yán)重依賴鍵值觀察獲得模型層和控制器層的變化通知的。對(duì)于不依賴控制器層類的應(yīng)用程序,鍵值觀察提供了一種簡(jiǎn)化的方法來(lái)實(shí)現(xiàn)檢查器并更新用戶界面值。
      與NSNotification不同,鍵值觀察中并沒(méi)有所謂的中心對(duì)象來(lái)為所有觀察者提供變化通知。取而代之地,當(dāng)有變化發(fā)生時(shí),通知被直接發(fā)送至處于觀察狀態(tài)的對(duì)象。NSObject提供這種基礎(chǔ)的鍵值觀察實(shí)現(xiàn)方法,你幾乎不用重寫(xiě)該方法。
      你可以觀察任意對(duì)象屬性,包括簡(jiǎn)單屬性,對(duì)一或?qū)Χ嚓P(guān)系。對(duì)多關(guān)系的觀察者將會(huì)被告知發(fā)生變化的類型,也就是任意發(fā)生變化的對(duì)象。
      鍵值觀察為所以對(duì)象提供自動(dòng)觀察兼容性。你可以通過(guò)禁用自動(dòng)觀察通知并實(shí)現(xiàn)手動(dòng)通知來(lái)篩選通知。

  • 注冊(cè)觀察者
      為了正確接收屬性的變更通知,觀察對(duì)象必須首先發(fā)送一個(gè)addObserver: forKeyPath: options: context: 消息至被觀察對(duì)象,用以傳送觀察對(duì)象和需要觀察的屬性的關(guān)鍵路徑,以便于其注冊(cè)。選項(xiàng)參數(shù)指定了發(fā)送變更通知時(shí)提供給觀察者的信息。使用NSKeyValueObservingOptionOld選項(xiàng)可以將初始對(duì)象值以變更字典中的一個(gè)項(xiàng)的形式提供給觀察者。指定NSKeyValueObservingOptionNew選項(xiàng)可以將新的值以一個(gè)項(xiàng)的形式添加至變更字典。你可以用逐為"|"同時(shí)使用這兩個(gè)常量來(lái)指定上述兩種類型的值。

person.name = @"xiao wang";改變姓名 person.name = @"xiao ming";
  /*
     作用:給對(duì)象綁定一個(gè)觀察者(監(jiān)聽(tīng)者)
     addObserver:   觀察者
     forKeyPath:    要監(jiān)聽(tīng)的屬性
     options:       選項(xiàng)(方法中拿到屬性值)
     context:       上下文一般為nil
   */
  //監(jiān)聽(tīng)變更舊的值
  [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld context:nil];
此時(shí)打?。?change = {
  kind = 1;
  old = "xiao wang";
}
  //監(jiān)聽(tīng)變更新的值
  [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
此時(shí)打印:
change = {
  kind = 1;
  new = "xiao ming";
}
  //同時(shí)監(jiān)聽(tīng)變更舊的值和新的值
  [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
此時(shí)打印:
change = {
  kind = 1;
  new = "xiao ming";
  old = "xiao wang";
}
  • 接收變更通知
      當(dāng)監(jiān)聽(tīng)的屬性發(fā)生變動(dòng)時(shí),觀察者收到observeValueForKeyPath: ofObject: change: context: 消息,觀察者必須實(shí)現(xiàn)這一方法。觸發(fā)觀察者通知的對(duì)象和鍵路徑、包含變更細(xì)節(jié)的字典,以及觀察者注冊(cè)時(shí)提交的上下文指針均被提交給觀察者
/**
*  當(dāng)監(jiān)聽(tīng)的屬性值發(fā)生改變是執(zhí)行
*
*  @param keyPath 發(fā)生改變的屬性
*  @param object  改變的屬性所屬的對(duì)象
*  @param change  改變的內(nèi)容
*  @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
  
  // 打印改變的內(nèi)容
  NSLog(@"change = %@",change);
}
打印結(jié)果如:
change = {
  kind = 1;
  new = "xiao ming";
  old = "xiao wang";
}

  • 移除觀察者身份
      你可以發(fā)送一條指定指定觀察者對(duì)象和鍵路徑的removeObserver: forKeyPath: 消息至被觀察的對(duì)象,來(lái)移除一個(gè)鍵值觀察者。
      //移除觀察者身份
      [person removeObserver:self forKeyPath:@"name"];
    
    
KVO 使用需要注意的一些地方
  • 在對(duì)象銷毀時(shí)要先移除觀察者身份,否則會(huì)報(bào)錯(cuò)
- (void)viewDidLoad {
  [super viewDidLoad];
  
  Person *person = [[Person alloc] init];
  
  person.name = @"xiao wang";
  
  //監(jiān)聽(tīng)變更新的值
  [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  
  person.name = @"xiao ming";
}
/**
*  當(dāng)監(jiān)聽(tīng)的屬性值發(fā)生改變是執(zhí)行
*
*  @param keyPath 發(fā)生改變的屬性
*  @param object  改變的屬性所屬的對(duì)象
*  @param change  改變的內(nèi)容
*  @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
  
  // 打印改變的內(nèi)容
  NSLog(@"change = %@",change);
}
  
在執(zhí)行完viewDidLoad方法后,person會(huì)被銷毀。然而viewDidLoad在person不需要再使用的時(shí)候并沒(méi)有移除person的觀察者身份會(huì)引起crash
change = {
  kind = 1;
  new = "xiao ming";
}
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7fa633d97950 of class Person was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x7fa633d920d0> (
<NSKeyValueObservance 0x7fa633d9ad80: Observer: 0x7fa633e13090, Key path: name, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x7fa633d9ac40>
)'
*** First throw call stack:
(
  0   CoreFoundation                      0x000000010ca92e65 __exceptionPreprocess + 165
  1   libobjc.A.dylib                     0x000000010c50bdeb objc_exception_throw + 48
  2   CoreFoundation                      0x000000010ca92d9d +[NSException raise:format:] + 205
  3   Foundation                          0x000000010c11d611 NSKVODeallocate + 294
  4   libobjc.A.dylib                     0x000000010c51fafe _ZN11objc_object17sidetable_releaseEb + 232
  5   ???áaá?§?áêü                        0x000000010c0089bc -[ViewController viewDidLoad] + 252
  6   UIKit                               0x000000010cfd5f98 -[UIViewController loadViewIfRequired] + 1198
  7   UIKit                               0x000000010cfd62e7 -[UIViewController view] + 27
  8   UIKit                               0x000000010ceacab0 -[UIWindow addRootViewControllerViewIfPossible] + 61
  9   UIKit                               0x000000010cead199 -[UIWindow _setHidden:forced:] + 282
  10  UIKit                               0x000000010cebec2e -[UIWindow makeKeyAndVisible] + 42
  11  UIKit                               0x000000010ce37663 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4131
  12  UIKit                               0x000000010ce3dcc6 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1760
  13  UIKit                               0x000000010ce3ae7b -[UIApplication workspaceDidEndTransaction:] + 188
  14  FrontBoardServices                  0x000000010f80b754 -[FBSSerialQueue _performNext] + 192
  15  FrontBoardServices                  0x000000010f80bac2 -[FBSSerialQueue _performNextFromRunLoopSource] + 45
  16  CoreFoundation                      0x000000010c9bea31 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
  17  CoreFoundation                      0x000000010c9b495c __CFRunLoopDoSources0 + 556
  18  CoreFoundation                      0x000000010c9b3e13 __CFRunLoopRun + 867
  19  CoreFoundation                      0x000000010c9b3828 CFRunLoopRunSpecific + 488
  20  UIKit                               0x000000010ce3a7cd -[UIApplication _run] + 402
  21  UIKit                               0x000000010ce3f610 UIApplicationMain + 171
  22  ???áaá?§?áêü                        0x000000010c00a51f main + 111
  23  libdyld.dylib                       0x000000010f1ce92d start + 1
  24  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
  
修復(fù)viewDidLoad:
- (void)viewDidLoad {
  [super viewDidLoad];
  
  Person *person = [[Person alloc] init];
  
  person.name = @"xiao wang";
  
  //監(jiān)聽(tīng)變更新的值
  [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  
  person.name = @"xiao ming";
  
  //移除觀察者身份
  [person removeObserver:self forKeyPath:@"name"];

}

  • 修改觀察的屬性的值需要用“.”語(yǔ)法去修改或者KVC,改下劃線屬性(__examTime),KVO并不能監(jiān)聽(tīng)到
    創(chuàng)建一個(gè)Student類
    Student.h文件
#import <Foundation/Foundation.h>
    
@interface Student : NSObject
    
// 離考試時(shí)間
@property (nonatomic, assign) int examTime;
    
@end

Student.m文件

#import "Student.h"
  
@implementation Student
  
/**
*  初始化方法
*
*  @return <#return value description#>
*/
- (instancetype)init{
    
  self = [super init];
    
  if (self != nil) {
        
      self.examTime = 10;
        
      //設(shè)置一個(gè)定時(shí)器,減少離考試的時(shí)間examTime
      [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeGone:) userInfo:nil repeats:YES];
        
  };
    
  return self;
}
  
//定時(shí)器方法
- (void)timeGone:(NSTimer *)timer{
    
  self.examTime--;
  /*
   self.examTime--;
   在ViewController 的viewDidLoad 方法中監(jiān)聽(tīng)
   Student *student = [[Student alloc] init];
     
   [student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
     
   是可以監(jiān)聽(tīng)的到時(shí)間變化的
   */
    
  //_examTime--;
  /*
   在ViewController 的viewDidLoad 方法中監(jiān)聽(tīng)
   Student *student = [[Student alloc] init];
     
   [student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
     
   是不能監(jiān)聽(tīng)的到時(shí)間變化的
   */
    
    
   //[self setValue:[NSNumber numberWithInt:_examTime] forKey:@"examTime"];
  /*
   在ViewController 的viewDidLoad 方法中監(jiān)聽(tīng)
   Student *student = [[Student alloc] init];
     
   [student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
     
   是可以監(jiān)聽(tīng)的到時(shí)間變化的
   */
    
    
   //[self setValue:[NSNumber numberWithInt:_examTime] forKey:@"_examTime"];
  /*
   在ViewController 的viewDidLoad 方法中監(jiān)聽(tīng)
   Student *student = [[Student alloc] init];
     
   [student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
   
   是不能監(jiān)聽(tīng)的到時(shí)間變化的原因是KVO注冊(cè)監(jiān)聽(tīng)的key是@"examTime", KCV改變的key是@"_examTime"
   */
  
}
  
@end
    
在ViewController的viewDidLoad方法中使用
- (void)viewDidLoad {
  [super viewDidLoad];
    
  Student *student = [[Student alloc] init];
    
  [student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
    
  /*
   這里的代碼存在問(wèn)題
   沒(méi)有添加移除student觀察者身份,但此處只是為了驗(yàn)證_examTime--不能觸發(fā)觀察者模式
   固不作進(jìn)一步優(yōu)化
   */
}
  
/**
*  當(dāng)監(jiān)聽(tīng)的屬性值發(fā)生改變是執(zhí)行
*
*  @param keyPath 發(fā)生改變的屬性
*  @param object  改變的屬性所屬的對(duì)象
*  @param change  改變的內(nèi)容
*  @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    
  // 打印改變的內(nèi)容
  NSLog(@"change = %@",change);
    
}
  
打印結(jié)果:
change = {
  kind = 1;
  old = 10;
}
change = {
  kind = 1;
  old = 9;
}
change = {
  kind = 1;
  old = 8;
}
change = {
  kind = 1;
  old = 7;
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容