iOS下KVO使用过程中的陷阱KVO,

【原】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";

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容