KVO
KVO,即key-value-observing,利用一个key来找到某个属性并监听其值得改变。其实这也是一种典型的观察者模式。
简单的说,kvo的用法非常简单。
- 添加观察者
- 在观察者中实现监听方法,observeValueForKeyPath: ofObject: change: context:(通过查阅文档可以知道,绝大多数对象都有这个方法,因为这个方法属于NSObject)
- 移除观察者
如果将一个对象设定成属性,这个属性是自动支持KVO的,如果这个对象是一个实例变量,那么,这个KVO是需要我们自己来实现的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| // // Student.h // Test // // Created by Apple on 2018/4/4. // Copyright © 2018年 王全金. All rights reserved. //
#import <Foundation/Foundation.h>
@interface Student : NSObject { NSString *_country; }
- (void)setCountry:(NSString *)country; - (NSString *)country;
@property (nonatomic, copy) NSString *name;
@end
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| // // Student.m // Test // // Created by Apple on 2018/4/4. // Copyright © 2018年 王全金. All rights reserved. //
#import "Student.h"
@implementation Student
- (void)setCountry:(NSString *)country { [self willChangeValueForKey:@"country"]; _country = country; [self didChangeValueForKey:@"country"]; }
- (NSString *)country { return _country; }
//手动实现getter和setter @synthesize name = _name; - (void)setName:(NSString *)name { _name = name; } - (NSString *)name { return _name; }
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { // 如果监测到键值为country,则指定为非自动监听对象 if ([key isEqualToString:@"country"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; }
@end
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| // // ViewController.m // Test // // Created by Apple on 2018/3/28. // Copyright © 2018年 王全金. All rights reserved. //
#import "ViewController.h" #import "Student.h"
@interface ViewController ()
@property (nonatomic, strong) Student *student;s
@end
@implementation RootViewController
- (void)viewDidLoad { [super viewDidLoad];
// 创建学生对象 _student = [Student new]; // 监听属性name [_student addObserver:self forKeyPath:@"name" // 属性 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; // 监听实例变量age [_student addObserver:self forKeyPath:@"country" // 实例变量 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; _student.name = @"Marks"; // 改变名字 _student.country = @"中国"; // 改变国家 }
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@", change); }
- (void)dealloc { [self removeObserver:_student forKeyPath:@"name"]; [self removeObserver:_student forKeyPath:@"country"]; }
@end
|
KVO的底层实现
当一个类的属性被观察的时候,系统会通过runtime动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter方法,而且系统将这个类的isa指针指向了派生类,从而实现了给监听的属性赋值时调用的是派生类的setter方法。重写的setter方法会在调用原setter方法前后,通知观察对象值得改变。

facebook开源的工具,KVOController ,是一个简单安全的 KVO(Key-value Observing,键-值 观察)工具,好像挺好用的。
KVC
KVC概述
KVC是Key Value Coding的简称。它是一种可以通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。
关键方法定义在 NSKeyValueCodingProtocol
KVC支持类对象和内建基本数据类型。
KVC使用
获取值
valueForKey: 传入NSString属性的名字。
valueForKeyPath: 属性的路径,xx.xx
valueForUndefinedKey 默认实现是抛出异常,可重写这个函数做错误处理
修改值
setValue:forKey:
setValue:forKeyPath:
setValue:forUnderfinedKey:
setNilValueForKey: 对非类对象属性设置nil时调用,默认抛出异常。
KVC键值查找
搜索单值成员
setValue:forKey:搜索方式
- 首先搜索setKey:方法。(key指成员变量名,首字母大写)
- 上面的setter方法没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。(NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES)
- 如果没有找到成员变量,调用setValue:forUnderfinedKey:
valueForKey:的搜索方式
- 首先按getKey,key,isKey的顺序查找getter方法,找到直接调用。如果是BOOL、int等内建值类型,会做NSNumber的转换。
- 上面的getter没找到,查找countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。
- 还没找到,查找countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。
- 还是没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。
- 再没找到,调用valueForUndefinedKey。
KVC实现分析
KVC运用了isa-swizzing技术。isa-swizzing就是类型混合指针机制。KVC通过isa-swizzing实现其内部查找定位。isa指针(is kind of 的意思)指向维护分发表的对象的类,该分发表实际上包含了指向实现类中的方法的指针和其他数据。
比如说如下的一行KVC代码:
1 2 3 4 5 6
| [site setValue:@"sitename" forKey:@"name"];
//会被编译器处理成 SEL sel = sel_get_uid(setValue:forKey); IMP method = objc_msg_loopup(site->isa,sel); method(site,sel,@"sitename",@"name");
|
每个类都有一张方法表,是一个hash表,值是还书指针IMP,SEL的名称就是查表时所用的键。
SEL数据类型:查找方法表时所用的键。定义成char*,实质上可以理解成int值。
IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。
KVC的内部机制:
一个对象在调用setValue的时候进行了如下操作:
- 根据方法名找到运行方法的时候需要的环境参数
- 他会从自己的isa指针结合环境参数,找到具体的方法实现接口。
- 再直接查找得来的具体的实现方法
KVC支持的聚合运算
- sum 求和
- max 最大值
- min 最小值
- avg 平均值
- count 数量
使用方法
新建一个类Person,里面存放一个属性age
1 2 3 4 5 6 7 8
| #import <Foundation/Foundation.h>
@interface Person : NSObject
/** 年龄 */ @property (nonatomic, assign) NSUInteger age;
@end
|
创建一个数组,存放Person对象
1 2 3 4 5 6
| NSMutableArray<Person *> *persons = [NSMutableArray array]; for (int i = 1; i <= 5; i++) { Person *p = [[Person alloc] init]; p.age = i; [persons addObject:p]; }
|
使用@sum @min @max @avg @count进行聚合运算
1 2 3 4 5 6 7 8 9 10 11
| NSInteger sum = [[persons valueForKeyPath:@"@sum.age"] integerValue]; NSInteger min = [[persons valueForKeyPath:@"@min.age"] integerValue]; NSInteger max = [[persons valueForKeyPath:@"@max.age"] integerValue]; float avg = [[persons valueForKeyPath:@"@avg.age"] floatValue]; NSInteger count = [[persons valueForKeyPath:@"@count.age"] integerValue];
NSLog(@"sum=%zd", sum); NSLog(@"min=%zd", min); NSLog(@"max=%zd", max); NSLog(@"avg=%g", avg); NSLog(@"count=%zd", count);
|
打印结果
1 2 3 4 5
| 2018-04-04 13:42:50.099122+0800 Test[11815:158919] sum=15 2018-04-04 13:42:50.099353+0800 Test[11815:158919] min=1 2018-04-04 13:42:50.099506+0800 Test[11815:158919] max=5 2018-04-04 13:42:50.099631+0800 Test[11815:158919] avg=3 2018-04-04 13:42:50.099867+0800 Test[11815:158919] count=5
|
数组中直接存放数值的情况
直接使用@运算符.floatValue
1 2 3 4 5
| NSArray<NSNumber *> *arr = @[@1, @2, @3, @4]; NSNumber *avg = [arr valueForKeyPath:@"@avg.floatValue"]; NSNumber *sum = [arr valueForKeyPath:@"@sum.integerValue"]; NSLog(@"avg=%@", avg); NSLog(@"sum=%@", sum);
|
打印结果
1 2
| 2018-04-04 13:47:56.108888+0800 Test[12017:162610] avg=2.5 2018-04-04 13:47:56.109200+0800 Test[12017:162610] sum=10
|