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

【原】iOS下KVO使用過程中的陷阱KVO讶凉,全稱為Key-Value Observing,是iOS中的一種設(shè)計(jì)模式山孔,用于檢測(cè)對(duì)象的某些屬性的實(shí)時(shí)變化情況并作出響應(yīng)懂讯。網(wǎng)上廣為流傳普及的一個(gè)例子是利用KVO檢測(cè)股票價(jià)格的變動(dòng),例如這里台颠。這個(gè)例子作為掃盲入門還是可以的,但是當(dāng)應(yīng)用場(chǎng)景比較復(fù)雜時(shí)蓉媳,里面的一些細(xì)節(jié)還是需要改進(jìn)的酪呻,里面有多個(gè)地方存在crash的危險(xiǎn)玩荠。本文旨在逐步遞進(jìn)深入地探討出一種目前比較健壯穩(wěn)定的KVO實(shí)現(xiàn)方案,彌補(bǔ)網(wǎng)上大部分教程的不足闷尿!首先填具,假設(shè)我們的目標(biāo)是在一個(gè)UITableViewController內(nèi)對(duì)tableview的contentOffset進(jìn)行實(shí)時(shí)監(jiān)測(cè)匆骗,很容易地使用KVO來實(shí)現(xiàn)為碉就。在初始化方法中加入:

[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];

在dealloc中移除KVO監(jiān)聽:[_tableView removeObserver:self forKeyPath:@"contentOffset" context:nil];添加默認(rèn)的響應(yīng)回調(diào)方法:

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

?[self doSomethingWhenContentOffsetChanges];

}

好了瓮钥,KVO實(shí)現(xiàn)就到此完美結(jié)束了,拜拜桨武。玻募。七咧。開個(gè)玩笑,肯定沒這么簡(jiǎn)單的艾栋,這樣的代碼太粗糙了蝗砾,當(dāng)你在controller中添加多個(gè)KVO時(shí)悼粮,所有的回調(diào)都是走同上述函數(shù)曾棕,那就必須對(duì)觸發(fā)回調(diào)函數(shù)的來源進(jìn)行判斷翘地。

判斷如下:

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

if (object == _tableView && [keyPath isEqualToString:@"contentOffset"])?

{

[self doSomethingWhenContentOffsetChanges];}

?}

你以為這樣就結(jié)束了嗎?答案是否定的勺远!我們假設(shè)當(dāng)前類(在例子中為UITableViewController)還有父類时鸵,并且父類也有自己綁定了一些其他KVO呢?我們看到宪塔,上述回調(diào)函數(shù)體中只有一個(gè)判斷某筐,如果這個(gè)if不成立南誊,這次KVO事件的觸發(fā)就會(huì)到此中斷了蜜托。但事實(shí)上橄务,若當(dāng)前類無法捕捉到這個(gè)KVO,那很有可能是在他的superClass重挑,或者super-superClass...中谬哀,上述處理砍斷了這個(gè)鏈严肪。

合理的處理方式應(yīng)該是這樣的:

- (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];}

} 這樣就結(jié)束了嗎驳糯?

答案仍舊是否定的。潛在的問題有可能出現(xiàn)在dealloc中對(duì)KVO的注銷上很洋。KVO的一種缺陷(其實(shí)不能稱為缺陷喉磁,應(yīng)該稱為特性)是协怒,當(dāng)對(duì)同一個(gè)keypath進(jìn)行兩次removeObserver時(shí)會(huì)導(dǎo)致程序crash卑笨,這種情況常常出現(xiàn)在父類有一個(gè)kvo赤兴,父類在dealloc中remove了一次桶良,子類又remove了一次的情況下。不要以為這種情況很少出現(xiàn)曲秉!當(dāng)你封裝framework開源給別人用或者多人協(xié)作開發(fā)時(shí)是有可能出現(xiàn)的承二,而且這種crash很難發(fā)現(xiàn)亥鸠。不知道你發(fā)現(xiàn)沒负蚊,目前的代碼中context字段都是nil盖桥,那能否利用該字段來標(biāo)識(shí)出到底kvo是superClass注冊(cè)的揩徊,還是self注冊(cè)的搓劫?回答是可以的齿税。我們可以分別在父類以及本類中定義各自的context字符串炊豪,比如在本類中定義context為@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer時(shí)指定移除的自身添加的observer。這樣iOS就能知道移除的是自己的kvo串绩,而不是父類中的kvo,避免二次remove造成crash芜壁。寫作本文來由:? iOS默認(rèn)不支持對(duì)數(shù)組的KVO,因?yàn)槠胀ǚ绞奖O(jiān)聽的對(duì)象的地址的變化慧妄,而數(shù)組地址不變窟蓝,而是里面的值發(fā)生了改變整個(gè)過程需要三個(gè)步驟 (與普通監(jiān)聽一致)

*? 第一步 建立觀察者及觀察的對(duì)象 ? ?

*? 第二步 處理key的變化(根據(jù)key的變化刷新UI)? ??

*? 第三步 移除觀察者*/[objc] view plain copy數(shù)組不能放在UIViewController里面窖铡,在這里面的數(shù)組是監(jiān)聽不到數(shù)組大小的變化的费彼,需要將需要監(jiān)聽的數(shù)組封裝到model里面<? model類為: 將監(jiān)聽的數(shù)組封裝到model里箍铲,不能監(jiān)聽UIViewController里面的數(shù)組兩個(gè)屬性 一個(gè) 字符串類的姓名颠猴,一個(gè)數(shù)組類的modelArray,我們需要的就是監(jiān)聽modelArray里面元素的變化[objc] view plain copy@interface model : NSObject? @property(nonatomic, copy)NSString *name;? @property(nonatomic, retain)NSMutableArray *modelArray;??

1 建立觀察者及觀察的對(duì)象?

?第一步? 建立觀察者及觀察的對(duì)象? ? [_modeladdObserver:selfforKeyPath:@"modelArray"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:NULL];??

第二步 處理key的變化(根據(jù)key的變化刷新UI)? ? 最重要的就是添加數(shù)據(jù)這里[objc] view plain copy不能這樣 [_model.modelArray addObject]方法翘瓮,需要這樣調(diào)用? [[_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"];? }? 以下附上本文代碼:代碼中涉及三點(diǎn)

1 根據(jù)數(shù)組動(dòng)態(tài)刷新tableview;2 定時(shí)器的使用(涉及循環(huán)引用問題)资盅;3 使用KVC優(yōu)化model的初始化代碼调榄。沒找到上傳整個(gè)工程的方法,

暫時(shí)附上代碼1? NSTimer相關(guān)//為防止controller和nstimer之間的循環(huán)引用呵扛,delegate指向當(dāng)前單例每庆,而不指向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相關(guān)

? ?@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相關(guān) ??

* 第一步 建立觀察者及觀察的對(duì)象? ?

*? 第二步 處理key的變化(根據(jù)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"];

}

//停止定時(shí)器

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

//定時(shí)添加數(shù)據(jù)

[self startTimer];

}

//添加定時(shí)器

-(void)startTimer

{

__block RootViewController *bself = self;

_timer = [NSTimer scheduledTimerWithTimeInterval:TimeInterval block:^{

[bself changeArray];

} repeats:YES];

}

//增加數(shù)組中的元素 自動(dòng)刷新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

對(duì)時(shí)鐘的運(yùn)用 根據(jù)時(shí)鐘更新uilabel的數(shù)值

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

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帖鸦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖侣滩,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件策添,死亡現(xiàn)場(chǎng)離奇詭異苦丁,居然都是意外死亡产上,警方通過查閱死者的電腦和手機(jī)沉桌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門冰抢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巢音,“玉大人,你說我怎么就攤上這事⊥核校” “怎么了彪杉?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長革答。 經(jīng)常有香客問我溪食,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上租谈,老公的妹妹穿的比我還像新娘劫拗。我一直安慰自己胁附,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衣吠,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音袍榆,去河邊找鬼胀屿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛包雀,可吹牛的內(nèi)容都是我干的宿崭。 我是一名探鬼主播葡兑,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼叙谨!你這毒婦竟也來了竟终?” 一聲冷哼從身側(cè)響起旺入,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤礼华,失蹤者是張志新(化名)和其女友劉穎咐鹤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體圣絮,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祈惶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扮匠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捧请。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖棒搜,靈堂內(nèi)的尸體忽然破棺而出疹蛉,到底是詐尸還是另有隱情,我是刑警寧澤力麸,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布可款,位于F島的核電站,受9級(jí)特大地震影響克蚂,放射性物質(zhì)發(fā)生泄漏筑舅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一陨舱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧版仔,春花似錦游盲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至然想,卻和暖如春莺奔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背变泄。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國打工令哟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妨蛹。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓屏富,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蛙卤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子狠半,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容