KVO : Key-Value-Observing
从字面量理解,是键-值-观察。
通过KVO,可以得到其他对象某个属性变更的通知。通过观察某个属性的变更情况,从而做出更新的操作。
KVO这一机制是基于NSKeyValueObserving协议的,Cocoa通过这个协议为所有遵循协议的对象提供了自动观察属性变化的能力。在NSObject中已经为我们实现了这一协议,所以我们不必去实现这个协议。
为什么要使用KVO?
有的朋友可能会有疑问,为什么要使用KVO呢?KVO能实现的我使用Setter方法同样能实现啊。其实不然KVO存在还是有它的价值的,那么接下来我们细数一下KVO的独特价值吧:
1.我们创建一两个setter方法感觉没什么,但是如果要观察的属性非常多,那么还能一一重写setter方法来实现吗?想必大家心里已有了答案,但是利用KVO则能很好的解决上述问题。
2.我们自定义的类是很容易改写setter方法的,但是如果你是用一个已经编译好了的类库时要监控其中一个属性时怎么办?难道还要去重写setter方法?如果使用KVO则很轻松解决问题。
3.使用KVO能够方便的记录变化前的值和变化后的值,不适用KVO你还要自己来解决这些问题。
4.KVO让你的代码看起来更加简洁清晰易于维护。
如何使用KVO?
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;//添加观察
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);//移除观察(方法1)
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;//移除观察(方法2)
1、添加观察只有一个方法。
[object addObserver: observer forKeyPath: @”frame” options: 0 context: nil];
参数:
object : 被观察对象
observer :观察对象
forKeyPath :属性的名字。如Student的name
options : 有四个值,分别是:
NSKeyValueObservingOptionNew 把更改之前的值提供给处理方法
NSKeyValueObservingOptionOld 把更改之后的值提供给处理方法
NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。
注:例子里的0就代表不带任何参数进去
context :可以带任何类型的参数。
2、移除观察有两个方法
可以发现,第一个移除的方法相比第二个移除的方法,多了一个参数:context
其可以精确开发者的意图,就是当同一个被观察者多次添加观察时,需要一个对象来精确区分是哪个,就可以用context来辨别。所以在addObserver的时候就应该指定context,而不是nil。
3、实现一个回调函数:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;
参数:
keyPath:属性的名字,如Student的name
object:被观察对象
change:对应options里的NSKeyValueObservingOptionNew、NSKeyValueObservingOptionOld等,如果options有多个值,则通过如下方法获取:[change objectForKey:@”old”],[change objectForKey:@”new”].
话不多说,通过一个简单的例子来说明:
Student.h
#import <Foundation/Foundation.h>
@interface Student : NSObject
@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)double age;
-(void)changeName:(NSString*)newName;
@end
Student.m
#import "Student.h"
@implementation Student
-(void)changeName:(NSString *)newName{
_name = newName;
}
@end
PageView.h
#import <Foundation/Foundation.h>
#import "Student.h"
@interface PageView : NSObject
-(id)init:(Student*)stu;
@end
PageView.m
#import "PageView.h"
@implementation PageView{
Student* student;
}
-(id)init:(Student *)stu{
if (self = [super init]) {
student = stu;
[stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
[stu addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
return self;
}
-(void)dealloc{
[student removeObserver:self forKeyPath:@"name"];
[student removeObserver:self forKeyPath:@"age"];
}
#pragma mark KVO回调
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"名字改变,旧名字:%@,新名字:%@",[change objectForKey:@"old"],[change objectForKey:@"new"]);
}else if ([keyPath isEqualToString:@"age"]){
NSLog(@"年龄改变,旧年龄:%@,新年龄:%@",[change objectForKey:@"old"],[change objectForKey:@"new"]);
}
}
@end
ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
ViewController.m
#import "ViewController.h"
#import "Student.h"
#import "PageView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Student *student = [[Student alloc]init];
student.name = @"wxx";
student.age = 23;
PageView *view = [[PageView alloc]init:student];
student.name = @"lyl";
student.age = 24;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
最终打印结果:
2015-12-07 11:08:49.909 myKVODemo[1475:46659] 名字改变,旧名字:wxx,新名字:lyl
2015-12-07 11:08:49.910 myKVODemo[1475:46659] 年龄改变,旧年龄:23,新年龄:24
当然啦,使用KVO需要避免一些陷阱;
1、removeObserver的时候要避免重复removeObserver,否则会报错:
Cannot remove an observer <PageView 0x7fc62b4c4ea0> for the key path "age" from <Student 0x7fc62b4bbeb0> because it is not registered as an observer.'
2、在同一个controller中,添加多个observer是走同一个回调的,因此需要判断清楚。
3、假设当前类(在例子中为UITableViewController)还有父类,并且父类也有自己绑定了一些其他KVO呢?我们看到,回调函数体中只有一个判断,如果这个if不成立,这次KVO事件的触发就会到此中断了。但事实上,若当前类无法捕捉到这个KVO,那很有可能是在他的superClass,或者super-superClass…中。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) {
[self doSomethingWhenContentOffsetChanges];
} }
合理的处理方式应该是这样的:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) {
[self doSomethingWhenContentOffsetChanges];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}