iOS概念之KVO(Key-Value Observing)

在一个复杂的,有状态的系统中,当一个对象的状态发生改变,如何通知系统,并对状态改变做出相应的行为是必需考虑的一个问题,在iOS中为这类问题提供了4种解决方法: 

1. NSNotifiactaion和NSNotificationCenter:通知中心 
2. Delegates:代理, 
3. Callback:回调, 
4. KVO(Key-Value Observing):键值观察 

这篇文章就来说说通过KVO通知系统状态发生改变的用法。 

KVO是什么? 
KVO是Object-C中定义的一个通知机制,其定义了一种对象间监控对方状态的改变,并做出反应的机制。对象可以为自己的属性注册观察者,当这个属性的值发生了改变,系统会对这些注册的观察者做出通知。其用途十分广泛,比方说,你的下载进度条是根据下载百分比决定的,那么,可以通过观察下载百分比的改变,刷新进度条的样式,来直观的反应下载进度等等。 

KVO的用法 
根据上面的描述,一个KVO的用法主要就涉及3个部分: 

1.为对象的属性注册观察者:对象通过调用下面这个方法为属性添加观察者 
Object-c代码  收藏代码
  1. - (void)addObserver:(NSObject *)observer  
  2.          forKeyPath:(NSString *)keyPath  
  3.             options:(NSKeyValueObservingOptions)options  
  4.             context:(void *)context  
            
  • observer: 观察者对象. 其必须实现方法observeValueForKeyPath:ofObject:change:context:.
  • keyPath: 被观察的属性,其不能为nil.
  • options: 设定通知观察者时传递的属性值,是传改变前的呢,还是改变后的,具体的设定可以这儿:https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSKeyValueObserving_Protocol/Reference/Reference.html#//apple_ref/doc/c_ref/NSKeyValueObservingOptions,通常设置为NSKeyValueObservingOptionNew。
  • context: 一些其他的需要传递给观察者的上下文信息,通常设置为nil


2.观察者接收通知,并做出处理:观察者通过实现下面的方法,完成对属性改变的响应: 
Object-c代码  收藏代码
  1. - (void)observeValueForKeyPath:(NSString *)keyPath  
  2.                       ofObject:(id)object  
  3.                         change:(NSDictionary *)change  
  4.                        context:(void *)context  


  • keyPath: 被观察的属性,其不能为nil.
  • object: 被观察者的对象.
  • change: 属性值,根据上面提到的Options设置,给出对应的属性值
  • context: 上面传递的context对象。
                
                  
3.清除观察者:对象通过下面这个方法移除观察者: 
Object-c代码  收藏代码
  1. - (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath  


4. 注意事项: 
使用KVO消息传递机制有两个要求:(1)观察者必须知道被观察对象,即在同一作用域。(2)观察者还需要知道被观察对象的生命周期,因为在销毁发送者对象之前,需要取消观察者的注册。 
另外:如果计划在Core Data对象上使用KVO,需要知道这跟一般的KVO使用方法有点不同。还必须结合Core Data的故障机制(faulting mechanism),一旦core data出现了故障,它将会触发其属性对应的观察者(即使这些属性值没有发生改变)。 

5. 一些好的实践 
  • (1)当一个观察者观察多个对象的相同属性(即不同Object,但是KeyPath相同),可通过设定静态的Context变量来区分不同的通知。
  • (2)使用NSStringFromSelector(@selector(method))来获取KeyPath,而不是直接通过NSString写属性名,这样编译器可以帮助发现属性名中的Typo。
  • (3)通过方法:+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key,通过一个Key观察多个属性值的改变。


吐槽 
这个API的设计真心的不好懂,另外把KeyPath设计为String也太什么了一点。 

下面是一个Demo, 分享之

先来看个引子:

有一个业务类:Walker,在这个类内部只负责关于业务逻辑的处理,比如负责从服务器传来的JSON中解析数据,或做其他业务数据上的处理。

有另一个类:ViewController,专门负责界面的交互与试图更新。其中,需要讲Walker的某些属性显示出来,并实时更新。

目前,据我所能想到的方法有以下几种:

方法1、直接的函数调用

Walker的类内部,把创建一个ViewController的对象,然后调用ViewController的修改界面的方法,把需要改动的属性值作为形参传给该函数。

这种方式最直观,因为它不需要绕任何弯子。但是,确实最糟的方法。因为Walker与ViewController这两个类从此紧紧耦合在一起了。记住这句话,处理业务逻辑的类,对外部的事情知道得越少越好。甚至于,要做到外部是否有VC(View Controller),有多少个VC都不影响我。假设这是一个项目,程序员A负责业务逻辑的处理,程序员B负责UI,则采取这种方式后,程序员A就受制于B,互相干扰。

 

方法2、利用消息通信机制(NSNotification)

在Walker内部建立消息中心NSNotificationCenter,把实例化之后的VC对象作为observer。Center建在Walker或者VC都无所谓,具体看我博客的另一篇文章【NSNotificationCenter总结】。这种方法比前面的方法好一些。但是有一个很大的缺点:如果Walker需要更改的属性很多而且很频繁,那么这种方式很不方便传值。而且,注意到了没,“把实例化后的VC对象作为observer”,始终逃不开在Walker内部对VC实例化。依旧是耦合着。

 

方法3、利用delegate

关于delegate的介绍有很多,这里就不多讲。但是在这种需求下用 delegate,有点“杀鸡用牛刀”感觉,成本较大,而且不直观。

 

方法4、利用KVO模式

所有的代码都将在ViewController中实现。对于Walker,它自己都不知道外部是否有VC,以及VC会怎用用我的属性。

 

三、Demo

 

复制代码
 1 //
 2 //  Walker.h
 3 //  KVOExample
 4 //
 5 //  Created by 老翁 on 13-7-29.
 6 //  Copyright (c) 2013年 wzl. All rights reserved.
 7 //
 8 
 9 #import <Foundation/Foundation.h>
10 
11 @interface Walker : NSObject
12 {
13     NSInteger _age;
14     NSString *_name;
15 }
16 
17 @property (nonatomic) NSInteger age;
18 @property (nonatomic, retain) NSString *name;
19 
20 - (id)initWithName:(NSString *)name age:(NSInteger)age;
21 
22 
23 @end
复制代码
复制代码
#import "Walker.h"

@implementation Walker

@synthesize age = _age;
@synthesize name = _name;

- (void)dealloc
{
    [_name release];
    [super dealloc];
}

- (id)initWithName:(NSString *)name age:(NSInteger)age
{
    if (self = [super init]) {
        _name = name;
        _age = age;
    }
    return  self;
}


@end
复制代码

 

ViewController代码:

复制代码
//
//  ViewController.h
//  KVOExample
//
//  Created by 老翁 on 13-7-29.
//  Copyright (c) 2013年 wzl. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "Walker.h"

@class Walker;

@interface ViewController : UIViewController
{
    Walker *_walker;
    UILabel *_ageLabel;
}

@property (nonatomic, assign) Walker *walker;

@end
复制代码

 

复制代码
 1 //
 2 //  ViewController.m
 3 //  KVOExample
 4 //
 5 //  Created by 老翁 on 13-7-29.
 6 //  Copyright (c) 2013年 wzl. All rights reserved.
 7 //
 8 
 9 #import "ViewController.h"
10 
11 @interface ViewController ()
12 
13 @end
14 
15 @implementation ViewController
16 
17 @synthesize walker = _walker;
18 
19 
20 - (void)dealloc
21 {
22     [_walker release];
23     [_ageLabel release];
24     [super dealloc];
25 }
26 
27 - (void)viewDidLoad
28 {
29     [super viewDidLoad];
30     // Do any additional setup after loading the view, typically from a nib.
31     _walker = [[Walker alloc] initWithName:@"老翁" age:25];
32     /* 关键步骤 */
33     [_walker addObserver:self
34               forKeyPath:@"age"
35                  options:NSKeyValueObservingOptionNew
36                  context:nil];
37     
38     
39     //UI initialization
40     UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
41     [btn setFrame:CGRectMake(120, 200, 100, 35)];
42     [btn setBackgroundColor:[UIColor lightGrayColor]];
43     [btn setTitle:@"增加5岁" forState:UIControlStateNormal];
44     [btn addTarget:self
45             action:@selector(buttonPressed)
46   forControlEvents:UIControlEventTouchUpInside];
47     [self.view addSubview:btn];
48     
49     _ageLabel = [[UILabel alloc] initWithFrame:CGRectMake(40, 150, 200, 35)];
50     _ageLabel.text = [NSString stringWithFormat:@"%@现在的年龄是: %d", _walker.name, _walker.age];
51     _ageLabel.backgroundColor = [UIColor clearColor];
52     [self.view addSubview:_ageLabel];
53     
54     
55 }
56 
57 - (void)buttonPressed
58 {
59     _walker.age += 5;
60 }
61 
62 /* KVO function, 只要object的keyPath属性发生变化,就会调用此函数*/
63 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
64 {
65     if ([keyPath isEqualToString:@"age"] && object == _walker) {
66         _ageLabel.text = [NSString stringWithFormat:@"%@现在的年龄是: %d", _walker.name, _walker.age];
67     }
68 }
69 
70 
71 
72 - (void)didReceiveMemoryWarning
73 {
74     [super didReceiveMemoryWarning];
75     // Dispose of any resources that can be recreated.
76 }
77 
78 @end
复制代码

 

  

点击了按钮之后,系统会调用 observeValueForKeyPath :函数,这个函数应该也是回调函数。在该函数内部做UI更新。我们以这种轻量级的方式达到了目的。

 



参考文章:http://beyondvincent.com/blog/2013/12/14/124-communication-patterns/ 
http://nshipster.com/key-value-observing/ 
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSKeyValueObserving_Protocol/Reference/Reference.html
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值