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";

}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容