iOS开发之KVO与KVC解析与实战

来源:互联网 发布:安徽中港金融数据诈骗 编辑:程序博客网 时间:2024/05/16 15:58

一、NSKeyValueCoding(KVC)


1.原理

 运用了一个isa-swizzling技术。isa-swizzling就是类型混合指针机制。KVC主要通过isa-swizzling,来实现其内部查找定位的。isa指针,如其名称所指,(就是is a kind of的意思),指向维护分发表的对象的类。该分发表实际上包含了指向实现类中的方法的指针,和其它数据。

    比如说如下的一行KVC的代码:

[site setValue:@"sitename" forKey:@"name"];
    就会被编译器处理成:

SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (site->isa,sel);
method(site, sel, @"sitename", @"name");
    首先介绍两个基本概念:

    (1)SEL数据类型:它是编译器运行Objective-C里的方法的环境参数。

    (2)IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。


    KVC 再某种程度上提供了访问器的替代方案。不过访问器方法是一个很好的东西,以至于只要是有可能,KVC也尽量再访问器方法的帮助下工作。为了设置或者返回对象属性,KVC按顺序使用如下技术:

1)检查是否存在名为-set<key>:的方法,并使用它做设置值。对于-get<key>和-set<key>:方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致;

2)如果上述方法不可用,则检查名为-_<key>、-_is<key>(只针对布尔值有效)、-_get<key>和-_set<key>:方法;

3)如果没有找到访问器方法,可以尝试直接访问实例变量。实例变量可以是名为:<key>或_<key>;

4)如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。


2.下面举一个小例子

KVC键值编码是Object-C为我们提供的一种对成员变量赋值的方法。在探讨其方法之前,我们先来看一个小例子:

首先,创建一个数据模型model类:

?
1
2
3
4
5
6
7
8
//.h文件
#import <Foundation/Foundation.h>
@interface Model : NSObject
{
    @public//将成员变量设置为公有的 以便其他文件有访问权限
    NSString * str;
}
@end

我们在其他文件中有两种方法str进行赋值和取值:

?
1
2
3
4
5
    Model * model = [[Model alloc]init];
    model->str=@"312";//普通方法赋值
    [model setValue:@"321" forKey:@"str"];//kvc赋值
    NSLog(@"%@",model->str);//普通方法取值
    NSLog(@"%@",[model valueForKey:@"str"]);//kvc取值

同样的,对于用@property声明的变量,使用kvc的效果和使用点语法,setter,getter方法的效果是一样的。


3.KVC有关函数方法详解

通过上面的例子,我们已经可以简单了解KVC是干什么的了,下面是一些常用方法。

+ (BOOL)accessInstanceVariablesDirectly;

这个方法类似一个开关,默认返回为YES,表示支持KVC方式赋值,也可以在子类中将其重写,如果返回为NO,则再进行KVC会抛出异常。


- (id)valueForKey:(NSString *)key;

通过键取值


- (void)setValue:(id)value forKey:(NSString *)key;

通过字符串键给成员变量赋值


- (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

系统默认实现的方法,验证一个键值是否有效


- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

将取到的值放入一个可变数组中


- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

将取到的值放入可变的有序集合中


- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

将取到的值放入可变的集合中


- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

上面这两个方法分别是通过路径赋值与取值,数据结构类似地图,比如在model类中有一个成员变量model2,在Model2类中有一个字符串,我们可以通过如下的方式赋值取值

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Model.h
#import "Model2.h"
@interface Model : NSObject
{
    @public
    NSString * str;
    Model2 * model2;
}
//Model2.h
@interface Model2 : NSObject
{
@public
    NSString * str2;
}
@end
//其他文件
    Model * model = [[Model alloc]init];
    Model2 * model2 = [[Model2 alloc]init];
    model->model2=model2;
    [model setValue:@"123" forKeyPath:@"model2.str2"];
    NSLog(@"%@",[model valueForKeyPath:@"model2.str2"]);


- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath NS_AVAILABLE(10_7, 5_0);
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

上面三个方法与前面类似,只是是从路径取值的。


- (id)valueForUndefinedKey:(NSString *)key;

这个方法可以获取没有提前定义的成员变量的值,比如运行时创建的,下面这个方法是给未定义的成员变量赋值


- (void)setValue:(id)value forUndefinedKey:(NSString *)key;

注意:这两个方法默认的实现会抛出异常,子类必须重写才能使用。


- (void)setNilValueForKey:(NSString *)key;

将成员变量置为nil


- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

根据键值获取键值对字典


- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

通过字典对成员变量同意赋值,经常使用


二、NSKeyValueObservingCustomization(KVO)

KVO是一种消息监听机制,可以在某个量发生变化的时候将消息传送给监听者,因此广泛用于传值,界面低耦合等逻辑中。KVO机制的核心是以下三个方法:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

使用这个方法注册一个监听者,参数解释如下:

observer:监听者对象

keyPath:监听的参数

options:监听选项

context:参数传递

监听的选项枚举如下:

?
1
2
3
4
5
6
7
8
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,//回调的字典中存放新值
    NSKeyValueObservingOptionOld = 0x02,//回调的字典中存放旧值
    NSKeyValueObservingOptionInitial ,//值改变前进行回调
    NSKeyValueObservingOptionPrior//改变前后都进行回调
 
};
//回调字典后面会解释


- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context ;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

这两个方法都是用来移除监听者


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

这个方法是监听对象数据改变时回调的方法,change是一个字典,字典中根据监听的选项不同,存放不同的值(新或者旧)。context是传递的参数。

代码示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
     model = [[Model alloc]init];
    //添加监听者
    [model addObserver:self forKeyPath:@"str" options:NSKeyValueObservingOptionNew context:@"321"];
    [model setValue:@"qw" forKey:@"str"];
}
 
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    if ([keyPath isEqualToString:@"str"]) {
        NSLog(@"%@",context);
    }
}

KVO 是基于KVC实现的,看下面的代码:

?
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
#pragma mark - KVO实现原理
     
    Person *person = [[Person alloc] init];
     
    [person setName:@"Jacedy"];
     
    // 设置监听
    [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
     
    [person setName:@"Jack"];
     
    self.person = person;
}
 
// 响应监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"%@ 对象的 %@ 属性改变了:%@", object, keyPath, change);
}
 
- (void)dealloc
{
    // 移除监听
    [self.person removeObserver:self forKeyPath:@"name"];
}

对代码进行断点跟踪发现如下:

233227_fQCe_580523.png

当添加了监听后:

233307_G0Db_580523.png

不难发现,person对象的isa指针由Person变成了NSKVONotifying_Perosn。其实,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。派生类在被重写的 setter 方法实现真正的通知机制:

?
1
2
3
4
5
6
- (void)setName:(NSString *)name
{
    [super setName:name];
     
    [监听器 observeValueForKeyPath:@"name"  ofObject:self  change:@{}  context:nil];
}

三、手动设定实例变量的KVO实现监听 
如果将一个对象设定成属性,这个属性是自动支持KVO的,如果这个对象是一个实例变量,那么,这个KVO是需要我们自己来实现的. 下面给出源码供大家参考:





最后给大家介绍下如何优雅的使用KVO

首先来吐槽下KVO:

  1. 需要手动移除观察者,且移除观察者的时机必须合适
  2. 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过 void * 指针;
  3. 需要覆写 -observeValueForKeyPath:ofObject:change:context: 方法,比较麻烦;
  4. 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的 if 进行判断;

虽然上述几个问题并不影响 KVO 的使用,不过这也足够成为笔者尽量不使用 KVO 的理由了。

如何优雅地解决上一节提出的几个问题呢?我们在这里只需要使用 Facebook 开源的 KVOController 框架就可以优雅地解决这些问题了。

如果想要实现同样的业务需求,当使用 KVOController 解决上述问题时,只需要以下代码就可以达到与上一节中完全相同的效果:

[self.KVOController observe:self.fizz                    keyPath:@"number"                    options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld                      block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString    *,id> * _Nonnull change) {                          NSLog(@"%@", change);                      }];

我们可以在任意对象上获得 KVOController 对象,然后调用它的实例方法 -observer:keyPath:options:block: 就可以检测某个对象对应的属性了,该方法传入的参数还是非常容易理解的,在 block 中也可以获得所有与 KVO 有关的参数。

使用 KVOController 进行键值观测可以说完美地解决了在使用原生 KVO 时遇到的各种问题。

  1. 不需要手动移除观察者;
  2. 实现 KVO 与事件发生处的代码上下文相同,不需要跨方法传参数;
  3. 使用 block 来替代方法能够减少使用的复杂度,提升使用 KVO 的体验;
  4. 每一个 keyPath 会对应一个属性,不需要在 block 中使用 if 判断 keyPath

0 0
原创粉丝点击