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,实现依赖键
@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
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"];输出结果:
五,最后的注意点
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];