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];