說完了前面一篇KVC,不能不說說它的應用KVO(Key-Value Observing)喽。KVO類似於ruby裡的hook功能,就是當一個對象屬性發生變化時,觀察者可以跟蹤變化,進而觀察或是修正這個變化,這是通過回調觀察者注冊的回調函數來完成的。要使用鍵值觀察,必須滿足3個條件:
1 被觀察對象必須對所觀察屬性使用符合KVC標准的存取器方法;
2 觀察者必須實現接受通知的方法(回調方法):-observeValue:forKeyPath:ofObject:change:context:,該方法在值發生變化時被調用,並且可以定制行為:比如同時接收新值和原值以及其他自定義信息;
3 觀察者通過-addObserver:forKeyPath:options:context:方法對特定對象的特定路徑進行注冊.
還要注意的是,在觀察者完成對被觀察者的觀察後,必須將自己移除,否則當觀察者釋放後,來自被觀察者的變化通知消息將無處可發,可能會導致引用崩潰。另外可以使用多個觀察者對同一個對象的同一個路徑進行觀察啊,類似於形成一個觀察鏈。
下面我將會寫一個簡單的例子,用代碼說明上述內容:
#import#define msg(...) NSLog(__VA_ARGS__) #define mki(x) [NSNumber numberWithInt:x] @interface Son:NSObject{ NSString *girl_friend_name; } @property NSString *girl_friend_name; -(void)think; -(void)fall_in_love_with:(NSString *)girl_name; @end @implementation Son @synthesize girl_friend_name; -(void)fall_in_love_with:(NSString *)girl_name{ self.girl_friend_name = girl_name; } -(void)think{ msg(@"My love girl is %@ ... Does baba know?",girl_friend_name); } @end @interface Baba:NSObject{ Son *son; } @property Son *son; @end @implementation Baba @synthesize son; -(id)initWithSon:(Son *)son_v{ self = [super init]; if(self){ son = son_v; [son addObserver:self forKeyPath:@"girl_friend_name" \ options:(NSKeyValueObservingOptionNew|\ NSKeyValueObservingOptionOld|NSKeyValueObservingOptionInitial) \ context:nil]; } return self; } -(void)observeValueForKeyPath:(NSString *)key_path \ ofObject:(id)obj change:(NSDictionary *)change \ context:(void *)context{ NSString *old_name = [change objectForKey:NSKeyValueChangeOldKey]; NSString *new_name = [change objectForKey:NSKeyValueChangeNewKey]; if(!old_name){ //NSKeyValueObservingOptionInitial選項的作用,第一次hook會發一次消息 //以後每次更改會發一次消息。 msg(@"Yes , It's first time to induction son's girl_name!!!"); }else{ if([new_name isEqualToString:@"libinbin"]){ [obj fall_in_love_with:@"fanbinbin"]; } msg(@"My son's %@ change from %@ to %@ ... That's OK? :(",\ key_path,old_name,new_name); } } -(void)dealloc{ [son removeObserver:self forKeyPath:@"girl_friend_name"]; son = nil; //[super dealloc]; } @end int main(int argc,char *argv[]) { @autoreleasepool{ Son *son = [[Son alloc] init]; [son fall_in_love_with:@"lili"]; [son think]; Baba *baba = [[Baba alloc] initWithSon:son]; [son fall_in_love_with:@"mimi"]; [son think]; //兒子本來喜歡的是libinbin,可是... [son fall_in_love_with:@"libinbin"]; //被他老爸用特異功能改成fanbinbin啦! [son think]; } return 0; }
簡單說明下:老爸自然是要隨時監控兒子的女朋友啊,可是這個老爸監視也就算了,還強行改變兒子的女朋友,這個就...隨便想的一個例子,就當fun吧。說明下主要方法:
-addObserver注冊對象的觀察者,其中的options的其他可選值如下,可以多選:
其中的context參數意義不明哦,如果哪位知道的,別忘了告訴老貓我一下啊。NSKeyValueObservingOptionInitial標志的含義參見代碼注釋吧。
-observeValueForKeyPath回調方法中的change是一個字典參數,其中包括:
其中NSKeyValueChangeKindKey指定了接收到得變化類型,可能有以下幾種值:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+PGltZyBzcmM9"/uploadfile/Collfiles/20140707/20140707084335219.png" alt="\">
書上的代碼在該方法定義的末尾部分增加了其對父類中同名方法的回溯調用:
[super observeValueForkeyPath:key_path ofObject:obj change:change context:ctx];
不過我在代碼中沒有寫這個。書上在觀察者的dealloc方法最後是有一句[super dealloc],但是這招在ARC下編譯時會出錯喽,暫時去掉吧。該段代碼運行結果如下:
apple@kissAir: objc_src$./k
2014-07-06 20:11:26.757 k[2109:507] My love girl is lili ... Does baba know?
2014-07-06 20:11:26.759 k[2109:507] Yes , It"s first time to induction son's girl_name!!!
2014-07-06 20:11:26.760 k[2109:507] My son's girl_friend_name change from lili to mimi ... That's OK? :(
2014-07-06 20:11:26.760 k[2109:507] My love girl is mimi ... Does baba know?
2014-07-06 20:11:26.761 k[2109:507] My son's girl_friend_name change from libinbin to fanbinbin ... That's OK? :(
2014-07-06 20:11:26.761 k[2109:507] My son's girl_friend_name change from mimi to libinbin ... That's OK? :(
2014-07-06 20:11:26.761 k[2109:507] My love girl is fanbinbin ... Does baba know?
爸爸監管的不錯,可是兒子不高興喽。hack兒子如何能讓老爸如此侵犯隱私啊?咋辦呢?不自動傳遞變更通知不就結了!為了實現手動通知,需要重寫Son的類方法+automaticallyNotifiesObserversForKey,來告知obj-c你不想自動通知觀察者自己的變化,注意是類方法哦!可以通過對特定的鍵名返回NO來完成:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{ if([key isEqualToString:@"girl_friend_name"]) return NO; return YES; }
在Son類的實現中加入以上類方法,重新編譯運行:
apple@kissAir: objc_src$./k
2014-07-06 20:37:36.726 k[2180:507] My love girl is lili ... Does baba know?
2014-07-06 20:37:36.729 k[2180:507] Yes , It's first time to induction son's girl_name!!!
2014-07-06 20:37:36.729 k[2180:507] My love girl is mimi ... Does baba know?
2014-07-06 20:37:36.730 k[2180:507] My love girl is libinbin ... Does baba know?
老爸”感應“不到喽!但是這樣長了老爸會懷疑的啦,腫麼寶貝兒子從不談戀愛呢?這個不行哦!於是乎聰明的hack兒子可以再手動的通知老爸篡改後的內容啊!具體為:在Son屬性寫者方法的變化之前調用-willChangeValueForKey,然後在變化之後調用-didChangeValueForKey,重新改寫寫者方法吧,修改後完整代碼如下:
#import#define msg(...) NSLog(__VA_ARGS__) #define mki(x) [NSNumber numberWithInt:x] @interface Son:NSObject{ NSString *girl_friend_name; } //在屬性默認為atomic的情況下,如果自定義寫者方法必須同時自定義讀者方法, //或者將屬性改為nonatomic模式。 @property(nonatomic) NSString *girl_friend_name; -(void)think; -(void)fall_in_love_with:(NSString *)girl_name; @end @implementation Son @synthesize girl_friend_name; -(void)fall_in_love_with:(NSString *)girl_name{ self.girl_friend_name = girl_name; } -(void)think{ msg(@"My love girl is %@ ... Does baba know?",girl_friend_name); } +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{ if([key isEqualToString:@"girl_friend_name"]) return NO; return YES; } -(void)setGirl_friend_name:(NSString *)name{ [self willChangeValueForKey:@"girl_friend_name"]; //讓老爸以為自己和女學霸談戀愛哦 girl_friend_name = @"女學霸"; [self didChangeValueForKey:@"girl_friend_name"]; //恢復本色吧,BAD BOY... :) girl_friend_name = name; } @end @interface Baba:NSObject{ Son *son; } @property Son *son; @end @implementation Baba @synthesize son; -(id)initWithSon:(Son *)son_v{ self = [super init]; if(self){ son = son_v; [son addObserver:self forKeyPath:@"girl_friend_name" \ options:(NSKeyValueObservingOptionNew|\ NSKeyValueObservingOptionOld|NSKeyValueObservingOptionInitial) \ context:nil]; } return self; } -(void)observeValueForKeyPath:(NSString *)key_path \ ofObject:(id)obj change:(NSDictionary *)change \ context:(void *)context{ NSString *old_name = [change objectForKey:NSKeyValueChangeOldKey]; NSString *new_name = [change objectForKey:NSKeyValueChangeNewKey]; if(!old_name){ //NSKeyValueObservingOptionInitial選項的作用,第一次hook會發一次消息 //以後每次更改會發一次消息。 msg(@"Yes , It's first time to induction son's girl_name!!!"); }else{ if([new_name isEqualToString:@"libinbin"]){ [obj fall_in_love_with:@"fanbinbin"]; } msg(@"My son's %@ change from %@ to %@ ... That's OK? :(",\ key_path,old_name,new_name); } } -(void)dealloc{ [son removeObserver:self forKeyPath:@"girl_friend_name"]; son = nil; //[super dealloc]; } @end int main(int argc,char *argv[]) { @autoreleasepool{ Son *son = [[Son alloc] init]; [son fall_in_love_with:@"lili"]; [son think]; Baba *baba = [[Baba alloc] initWithSon:son]; [son fall_in_love_with:@"mimi"]; [son think]; //兒子本來喜歡的是libinbin,可是... [son fall_in_love_with:@"libinbin"]; //被他老爸用特異功能改成fanbinbin啦! [son think]; } return 0; }
編譯運行結果如下:
apple@kissAir: objc_src$./k
2014-07-06 20:58:59.284 k[2270:507] My love girl is lili ... Does baba know?
2014-07-06 20:58:59.286 k[2270:507] Yes , It's first time to induction son's girl_name!!!
2014-07-06 20:58:59.287 k[2270:507] My son's girl_friend_name change from lili to 女學霸 ... That's OK? :(
2014-07-06 20:58:59.287 k[2270:507] My love girl is mimi ... Does baba know?
2014-07-06 20:58:59.288 k[2270:507] My son's girl_friend_name change from mimi to 女學霸 ... That's OK? :(
2014-07-06 20:58:59.288 k[2270:507] My love girl is libinbin ... Does baba know?
這回該老爸傻傻的以為兒子一直在和女學霸貪戀喽,各位童鞋當年也是這樣糊弄老爸老媽的吧,嘿嘿。