【原】iOS下KVO使用过程中的陷阱KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应。网上广为流传普及的一个例子是利用KVO检测股票价格的变动,例如这里。这个例子作为扫盲入门还是可以的,但是当应用场景比较复杂时,里面的一些细节还是需要改进的,里面有多个地方存在crash的危险。本文旨在逐步递进深入地探讨出一种目前比较健壮稳定的KVO实现方案,弥补网上大部分教程的不足!首先,假设我们的目标是在一个UITableViewController内对tableview的contentOffset进行实时监测,很容易地使用KVO来实现为。在初始化方法中加入:
[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
在dealloc中移除KVO监听:[_tableView removeObserver:self forKeyPath:@"contentOffset" context:nil];添加默认的响应回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
[self doSomethingWhenContentOffsetChanges];
}
好了,KVO实现就到此完美结束了,拜拜。。。开个玩笑,肯定没这么简单的,这样的代码太粗糙了,当你在controller中添加多个KVO时,所有的回调都是走同上述函数,那就必须对触发回调函数的来源进行判断。
判断如下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"])
{
[self doSomethingWhenContentOffsetChanges];}
}
你以为这样就结束了吗?答案是否定的!我们假设当前类(在例子中为UITableViewController)还有父类,并且父类也有自己绑定了一些其他KVO呢?我们看到,上述回调函数体中只有一个判断,如果这个if不成立,这次KVO事件的触发就会到此中断了。但事实上,若当前类无法捕捉到这个KVO,那很有可能是在他的superClass,或者super-superClass...中,上述处理砍断了这个链。
合理的处理方式应该是这样的:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) {
[self doSomethingWhenContentOffsetChanges];}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];}
} 这样就结束了吗?
答案仍旧是否定的。潜在的问题有可能出现在dealloc中对KVO的注销上。KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个kvo,父类在dealloc中remove了一次,子类又remove了一次的情况下。不要以为这种情况很少出现!当你封装framework开源给别人用或者多人协作开发时是有可能出现的,而且这种crash很难发现。不知道你发现没,目前的代码中context字段都是nil,那能否利用该字段来标识出到底kvo是superClass注册的,还是self注册的?回答是可以的。我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash。写作本文来由: iOS默认不支持对数组的KVO,因为普通方式监听的对象的地址的变化,而数组地址不变,而是里面的值发生了改变整个过程需要三个步骤 (与普通监听一致)
* 第一步 建立观察者及观察的对象
* 第二步 处理key的变化(根据key的变化刷新UI)
* 第三步 移除观察者*/[objc] view plain copy数组不能放在UIViewController里面,在这里面的数组是监听不到数组大小的变化的,需要将需要监听的数组封装到model里面< model类为: 将监听的数组封装到model里,不能监听UIViewController里面的数组两个属性 一个 字符串类的姓名,一个数组类的modelArray,我们需要的就是监听modelArray里面元素的变化[objc] view plain copy@interface model : NSObject @property(nonatomic, copy)NSString *name; @property(nonatomic, retain)NSMutableArray *modelArray;
1 建立观察者及观察的对象
第一步 建立观察者及观察的对象 [_modeladdObserver:selfforKeyPath:@"modelArray"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:NULL];
第二步 处理key的变化(根据key的变化刷新UI) 最重要的就是添加数据这里[objc] view plain copy不能这样 [_model.modelArray addObject]方法,需要这样调用 [[_model mutableArrayValueForKey:@"modelArray"] addObject:str];原因稍后说明。 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context[objc] view plain copy{ if ([keyPath isEqualToString:@"modelArray"]) { [_tableView reloadData]; } }
第三步 移除观察者[objc] view plain copyif (_model != nil) { [_model removeObserver:self forKeyPath:@"modelArray"]; } 以下附上本文代码:代码中涉及三点
1 根据数组动态刷新tableview;2 定时器的使用(涉及循环引用问题);3 使用KVC优化model的初始化代码。没找到上传整个工程的方法,
暂时附上代码1 NSTimer相关//为防止controller和nstimer之间的循环引用,delegate指向当前单例,而不指向controller
@interface NSTimer (DelegateSelf)
+(NSTimer *)scheduledTimerWithTimeInterval:(int)timeInterval block:(void(^)())block repeats:(BOOL)yesOrNo;
@end
#import "NSTimer+DelegateSelf.h"
@implementation NSTimer (DelegateSelf)
+(NSTimer *)scheduledTimerWithTimeInterval:(int)timeInterval block:(void(^)())block repeats:(BOOL)yesOrNo {
return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(callBlock:) userInfo:[block copy] repeats:yesOrNo]; }
+(void)callBlock:(NSTimer *)timer {
void(^block)() = timer.userInfo;
if (block != nil) { block(); } }
@end
2 model相关
@interface model : NSObject
@property(nonatomic, copy)NSString *name; @property(nonatomic, retain)NSMutableArray *modelArray;
-(id)initWithDic:(NSDictionary *)dic; @end
#import "model.h"
@implementation model
-(id)initWithDic:(NSDictionary *)dic {
self = [super init];
if (self) {
[self setValuesForKeysWithDictionary:dic];
}
return self; }
-(void)setValue:(id)value forUndefinedKey:(NSString *)key { NSLog(@"undefine key ---%@",key); } @end
3 UIViewController相关
* 第一步 建立观察者及观察的对象
* 第二步 处理key的变化(根据key的变化刷新UI)
* 第三步 移除观察者 */
#import "RootViewController.h"
#import "NSTimer+DelegateSelf.h"
#import "model.h"
#define TimeInterval 3.0
@interface RootViewController ()
@property(nonatomic, retain)NSTimer *timer;
@property(nonatomic, retain)UITableView *tableView;
@property(nonatomic, retain)model *model;
@end
@implementation RootViewController
//注意在什么地方注销观察者
- (void)dealloc
{
//第三步
if (_model != nil) {
[_model removeObserver:self forKeyPath:@"modelArray"];
}
//停止定时器
if (_timer != nil) {
[_timer invalidate];
_timer = nil;
}
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
NSDictionary *dic = [NSDictionary dictionaryWithObject:[NSMutableArray arrayWithCapacity:0] forKey:@"modelArray"];
self.model = [[model alloc] initWithDic:dic];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//第一步
[_model addObserver:self forKeyPath:@"modelArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:_tableView];
//定时添加数据
[self startTimer];
}
//添加定时器
-(void)startTimer
{
__block RootViewController *bself = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:TimeInterval block:^{
[bself changeArray];
} repeats:YES];
}
//增加数组中的元素 自动刷新tableview
-(void)changeArray
{
NSString *str = [NSString stringWithFormat:@"%d",arc4random()%100];
[[_model mutableArrayValueForKey:@"modelArray"] addObject:str];
}
//第二步 处理变化
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context
{
if ([keyPath isEqualToString:@"modelArray"]) {
[_tableView reloadData];
}
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [_model.modelArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
{
static NSString *cellidentifier = @"cellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellidentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellidentifier];
}
cell.textLabel.text = _model.modelArray[indexPath.row];
return cell;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
对时钟的运用 根据时钟更新uilabel的数值
-(void)updateLabel:(CGFloat)percent withAnimationTime:(CGFloat)animationTime{
CGFloat startPercent = [self.text floatValue];
CGFloat endPercent = percent*10;
CGFloat intever = animationTime/fabsf(endPercent - startPercent);
timer = [NSTimer scheduledTimerWithTimeInterval:intever target:self selector:@selector(IncrementAction:) userInfo:[NSNumber numberWithFloat:percent] repeats:YES];
[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
}
-(void)IncrementAction:(NSTimer *)time{
CGFloat change = [self.text integerValue];
CGFloat tt=[time.userInfo integerValue];
CGFloat dd=[time.userInfo floatValue]-tt;
if(change < [time.userInfo floatValue]){
change++;
}
else{
change--;
}
self.text = [NSString stringWithFormat:@"%.1f",(change+dd)];
if ([self.text integerValue] == [time.userInfo integerValue]) {
[time invalidate];
}
}
-(void)clear{
self.text = @"0";
}