在iOS开发中,苹果提供了许多机制给我们进行回调,代理,通知,block等。其中KVO(key-value-observing)是一种很实用的监听回调机制,KVO又基于KVC(key-value-coding)。
1. KVC
KVC就是键值编码,可以对私有变量进行赋值。主要通过isa-swizzling(类型混合指针机制),来实现其内部查找定位的。isa指针维护分发表的对象的类,该分发表实际上包含了指向实现类中的方法的指针,和其它数据。
[person setValue:@"myName" forKey:@"name"];
这行代码的底层实现原理如下:
- 首先系统会通过isa指针找到setName方法,通过setter方法进行设置;
- 如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作;
- 如果上面的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
2. KVO
KVO就是键值观察,当一个类的属性被观察的时候,系统会通过runtime动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter方法,而且系统将这个类的isa指针指向了派生类,从而实现了给监听的属性赋值时调用的是派生类的setter方法。重写的setter方法会在调用原setter方法前后,通知观察对象值得改变。
而给监听的属性赋值正好就是KVC要做的事情,所以说KVO是基于KVC实现的。
为了进一步探索KVO的原理,代码亲测如下:
NSLog(@"address: %p", self.tableView);
NSLog(@"class method: %@", self.tableView.class);
NSLog(@"description method: %@", self.tableView);
NSLog(@"get class: %@", object_getClass(self.tableView));
[self.tableView addObserver: self forKeyPath: @"contentOffset" options: NSKeyValueObservingOptionNew context: nil];
NSLog(@"===================================================");
NSLog(@"address: %p", self.tableView);
NSLog(@"class method: %@", self.tableView.class);
NSLog(@"description method: %@", self.tableView);
NSLog(@"get class %@", object_getClass(self.tableView));
输出结果:
2017-12-7 15:02:33.216 AndyTest[1287:54832] address: 0x7f927a81d200
2017-12-7 15:02:33.216 AndyTest[1287:54832] class method: UITableView
2017-12-7 15:02:33.216 AndyTest[1287:54832] description method: <UITableView: 0x7f927a81d200; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f927971f9a0>; layer = <CALayer: 0x7f9279706f50>; contentOffset: {0, 0}; contentSize: {600, 0}>
2017-12-7 15:02:33.216 AndyTest[1287:54832] get class: UITableView
2017-12-7 15:02:33.216 AndyTest[1287:54832] ===================================================
2017-12-7 15:02:33.216 AndyTest[1287:54832] address: 0x7f927a81d200
2017-12-7 15:02:33.216 AndyTest[1287:54832] class method: UITableView
2017-12-7 15:02:33.216 AndyTest[1287:54832] description method: <UITableView: 0x7f927a81d200; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f927971f9a0>; layer = <CALayer: 0x7f9279706f50>; contentOffset: {0, 0}; contentSize: {600, 0}>
2017-12-7 15:02:33.216 AndyTest[1287:54832] get class NSKVONotifying_UITableView
除了通过 object_getClass 获取的类型之外,其他都是一样的。这也就说明了 NSKVONotifying_UITableView就是isa指针指向的派生类。
3. 实例应用
我们假设在一个界面上用文字属性展示人物的年龄,我们给这个年龄添加一个监听者,当点击屏幕时年龄发生变化,我们就让展示的文字属性值跟随改变。
person类的.h
@interface Person : NSObject
/** age */
@property(nonatomic) NSInteger age;
@end
视图控制器的.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
/** person */
@property(nonatomic, strong) Person * person;
/** 展示的文字 */
@property(nonatomic, strong) UILabel *personAge;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//创建person对象,并对年龄年龄的变化添加监听
Person *person = [Person new];
person.age = 10;
self.person = person;
UILabel *personAge = [UILabel new];
personAge.frame = CGRectMake(100, 100, 100, 100);
personAge.text = [NSString stringWithFormat:@"%zd",person.age];
[self.view addSubview:personAge];
self.personAge = personAge;
//添加观察者
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}
/** 点击屏幕出发年龄改变 */
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.age = 20;
//KVC
//[self.person setValue:@20 forKey:@"age"];
}
/** 在观察者中实现监听方法 */
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
/**
NSLog(@"keyPath=%@,object=%@,change=%@,context=%@",keyPath,object,change,context);
keyPath=age,object=<Person: 0x7fe64af086e0>,change={
kind = 1;
new = 20;
},context=(null)
*/
NSLog(@"keyPath=%@,object=%@,change=%@,context=%@",keyPath,object,change,context);
//这里需要将NSNumber类型转换为字符串类型
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
NSString *ageStr = [numberFormatter stringFromNumber:[change objectForKey:@"new"]];
self.personAge.text = ageStr;
}
@end
最后要像通知一样,要在dealloc方法里面移除监听
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}