Marks

iOS开发者,喜欢研究技术。

0%

深入理解KVO和KVC

KVO

KVO,即key-value-observing,利用一个key来找到某个属性并监听其值得改变。其实这也是一种典型的观察者模式。
简单的说,kvo的用法非常简单。

  1. 添加观察者
  2. 在观察者中实现监听方法,observeValueForKeyPath: ofObject: change: context:(通过查阅文档可以知道,绝大多数对象都有这个方法,因为这个方法属于NSObject)
  3. 移除观察者

如果将一个对象设定成属性,这个属性是自动支持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:搜索方式

    1. 首先搜索setKey:方法。(key指成员变量名,首字母大写)
    2. 上面的setter方法没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。(NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES)
    3. 如果没有找到成员变量,调用setValue:forUnderfinedKey:
  • valueForKey:的搜索方式

    1. 首先按getKey,key,isKey的顺序查找getter方法,找到直接调用。如果是BOOL、int等内建值类型,会做NSNumber的转换。
    2. 上面的getter没找到,查找countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。
    3. 还没找到,查找countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。
    4. 还是没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。
    5. 再没找到,调用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