1. 程式人生 > >Objective-C之KVO(鍵值監聽)

Objective-C之KVO(鍵值監聽)

一,KVO的定義

KVO(Key-Value Observing),俗稱鍵值監聽。它提供一種機制,當指定的物件的屬性被修改後,則物件就會接受到通知。簡單的說就是每次指定的被觀察的物件的屬性被修改後,KVO就會自動通知相應的觀察者了。
KVO是“觀察者”設計模式的一種應用,利用它可以很容易實現檢視元件和資料模型的分離,當資料模型的屬性值改變之後作為監聽器的檢視元件就會被激發,激發時就會回撥監聽器自身。這種模式有利於兩個類間的解耦合,尤其是對於業務邏輯與檢視控制 這兩個功能的解耦合。
和KVC類似,在ObjC中要實現KVO則必須實現NSKeyValueObServing協議,但不用擔心,因為NSObject已經實現了該協議,因此幾乎所有的ObjC物件都可以使用KVO.
KVO常用的方法
 1>註冊指定Key路徑的監聽器

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
相關引數:
observer:觀察者,也就是KVO通知的訂閱者。訂閱著必須實現
observeValueForKeyPath:ofObject:change:context:方法
keyPath:描述將要觀察的屬性,相對於被觀察者。 
options:KVO的一些屬性配置;有四個選項。 

   options所包括的內容:

   NSKeyValueObservingOptionNew:change字典包括改變後的值 
   NSKeyValueObservingOptionOld:   change字典包括改變前的值 
   NSKeyValueObservingOptionInitial:註冊後立刻觸發KVO通知 
   NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
context: 上下文,這個會傳遞到訂閱著的函式中,用來區分訊息,所以應當是不同的。
注意:不要忘記解除註冊,否則會導致資源洩露
 2>刪除指定Key路徑的監聽器


  - (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath
  - (void)removeObserver:(NSObject *)observer     forKeyPath:(NSString *)keyPath  context:(void *)context
 3>回撥監聽
  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
  keyPath:被監聽的keyPath , 用來區分不同的KVO監聽。 
  object: 被觀察修改後的物件(可以通過object獲得修改後的值) 
  change:儲存資訊改變的字典(可能有舊的值,新的值等) 
  context:上下文,用來區分不同的KVO監聽
KVO的使用步驟也比較簡單
 1>註冊,指定被觀察者的屬性
 2> 實現回撥方法
 3>移除觀察
例項(ARC)

#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property(nonatomic,strong) Person * person;
@end
@implementation ViewController
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
    if(self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]){
        [self testKVO];
    }
    return self;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self ChangeNameValue];
}
/*1.註冊,指定被觀察者的屬性*/
-(void)testKVO{
   Person * testPerson = [[Person alloc]init];
   self.person = testPerson;
   [testPerson addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
/*2.實現回撥方法*/
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Name is changed! new = %@",[change valueForKey:NSKeyValueChangeNewKey]);
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
/*3.移除通知*/
-(void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name" context:nil];
}
//改變name的屬性,測試結果
-(void)ChangeNameValue{
    [self.person setValue:@"你妹" forKey:@"name"];
}

結果:


二,KVO的典型使用場景(model 與 view的同步)

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property(nonatomic,strong) Person * person;
@property(nonatomic,strong) UILabel * newsValue;//展示新值
@property(nonatomic,strong) UILabel * oldValue;//展示舊值
@property(nonatomic,strong) UIButton * TouchButton; //隨機button
@end
@implementation ViewController
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
    if(self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]){
        [self testKVO];//註冊KVO
    }
    return self;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self setViewSegment];//佈局View
}

/*1.註冊,指定被觀察者的屬性*/
-(void)testKVO{
   Person * testPerson = [[Person alloc]init];
   self.person = testPerson;
   [testPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
/*2.實現回撥方法*/
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"age"]) {
        NSNumber * old = [change objectForKey:NSKeyValueChangeOldKey];
        NSNumber * new = [change objectForKey:NSKeyValueChangeNewKey];
        self.newsValue.text =[NSString stringWithFormat:@"%@",old];
        self.oldValue.text =[NSString stringWithFormat:@"%@",new];
        NSLog(@"Name is changed! new = %@",[change valueForKey:NSKeyValueChangeNewKey]);
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
/*3.移除通知*/
-(void)dealloc{
    [self.person removeObserver:self forKeyPath:@"age" context:nil];
}
-(void)setViewSegment{
    self.newsValue = [[UILabel alloc]initWithFrame:CGRectMake(150, 50, 75, 40)];
    self.newsValue.textColor = [UIColor blueColor];
    self.newsValue.text = @"00";
    self.newsValue.textAlignment =NSTextAlignmentCenter;
    [self.view addSubview:self.newsValue];
    
    self.oldValue = [[UILabel alloc]initWithFrame:CGRectMake(150, 110, 75, 40)];
    self.oldValue.textColor = [UIColor redColor];
    self.oldValue.text = @"00";
    self.oldValue.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:self.oldValue];
    
    self.TouchButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [self.TouchButton setTitle:@"Random" forState:UIControlStateNormal];
    [self.TouchButton setFrame:CGRectMake(0, 0, 100, 60)];
    [self.TouchButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [self.TouchButton setCenter:CGPointMake(self.view.bounds.size.width/2, 200)];
    [self.TouchButton addTarget:self action:@selector(touchButtonAction:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.TouchButton];
}
-(void)touchButtonAction:(UIButton *)sender{
    
    self.person.age =arc4random()%100;//隨機
}

三,手動KVO

自動生成的KVO固然很好,但是它的靈活性,比較差.手動通知的好處就是,可以靈活加上自己想要的判斷條件

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

-(void)setAge:(NSUInteger)age{
    if (age < 22) {
        return;
    }
    [self willChangeValueForKey:@age];
    _age = age;
    [self didChangeValueForKey:@age] <span style="font-family: Arial, Helvetica, sans-serif;">}</span>
其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設定對該 key 不自動傳送通知(返回 NO 即可)。這裡要注意,對其它非手動實現的 key,要轉交給 super 來處理。
+(BOOL)automaticallyNotifiesObserversOfAge{
    return NO;
}

四,鍵值觀察依賴鍵

1,觀察依賴鍵
     觀察依賴鍵的方式與前面描述的一樣,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中新增處理變更通知的程式碼

    有時候一個屬性的值依賴於另一物件中的一個或多個屬性,如果這些屬性中任一屬性的值發生變更,被依賴的屬性值也應當為其變更進行標記。因此,object 引入了依賴鍵。

#import "TargetWrapper.h"
- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}
2,實現依賴鍵
     在這裡,觀察的是 TargetWrapper 類的 information 屬性,該屬性是依賴於 Target 類的 age 和 grade 屬性。為此,我在 Target 中添加了 grade 屬性:
@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end
@implementation Target

@end
下面來看看 TragetWrapper 中的依賴鍵屬性是如何實現的。
@class Target;

@interface TargetWrapper : NSObject
{
@private
    Target * _target;
}

@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;

-(id) init:(Target *)aTarget;

@end

#import "TargetWrapper.h"
#import "Target.h"

@implementation TargetWrapper

@synthesize target = _target;

-(id) init:(Target *)aTarget
{
    self = [super init];
    if (nil != self) {
        _target = [aTarget retain];
    }
    
    return self;
}

-(void) dealloc
{
    self.target = nil;
    [super dealloc];
}

- (NSString *)information
{
    return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}

- (void)setInformation:(NSString *)theInformation
{
    NSArray * array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

+ (NSSet *)keyPathsForValuesAffectingInformation
{
    NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
    return keyPaths;
}

//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
//    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//    NSArray * moreKeyPaths = nil;
//    
//    if ([key isEqualToString:@"information"])
//    {
//        moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
//    }
//    
//    if (moreKeyPaths)
//    {
//        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
//    }
//    
//    return keyPaths;
//}

@end

首先,要手動實現屬性 information 的 setter/getter 方法,在其中使用 Target 的屬性來完成其 setter 和 getter。
其次,要實現 keyPathsForValuesAffectingInformation  或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統 information 屬性依賴於哪些其他屬性,這兩個方法都返回一個key-path 的集合。在這裡要多說幾句,如果選擇實現 keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結果 set,然後判斷 key 是不是目標 key,如果是就將依賴屬性的 key-path 結合追加到 super 返回的結果 set 中,否則直接返回 super的結果。
在這裡,information 屬性依賴於 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發生變化,information 的觀察者都會得到通知。
3,使用示例:
Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];
TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
          forKeyPath:@"information"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:[TargetWrapper class]];
[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];
輸出結果:
 >> class: TargetWrapper, Information changed
      old information is 0#10
      new information is 0#30
>> class: TargetWrapper, Information changed
     old information is 0#30
     new information is 1#30

五,最後的注意點

KVO要提到的幾點
KVO和Context
由於Context通常用來區分不同的KVO,所以context的唯一性很重要。通常,我的使用方式是通過在當前.m檔案裡用靜態變數定義。
static void * privateContext = 0;
KVO與執行緒
KVO的響應和KVO觀察的值變化是在一個執行緒上的,所以,大多數時候,不要把KVO與多執行緒混合起來。除非能夠保證所有的觀察者都能執行緒安全的處理KVO
KVO監聽變化的值
改變前和改變後分別為
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];