入門篇
KVO是什麼?
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO 是 Objective-C 對觀察者模式(Observer Pattern)的實現。也是 Cocoa Binding 的基礎。當被觀察對象的某個屬性發生更改時,觀察者對象會獲得通知,並且得知此時這個屬性的具體值。
有點繞口。簡單點說,比如你監視你老婆,你老婆跟老王跑了,你會立即知道你老婆是跟老王跑了,並且也會知道上次是跟張三跑的。
KVO如何使用?
最好的使用入門指南:Introduction to Key-Value Observing Programming Guide
基礎篇
KVO實現原理 – In Apple Way
蘋果的實現方式用猿哥這張經典的圖理解起來還不錯,但是可能不太適合新手。
KVO的實現是經典的添加中間層的思想,雖然用了isa-swizzling,但是很符合邏輯,並沒有什麼黑魔法。我們來個生活中的實例講解下:
我們假設有一個叫Foo的女孩,有一個叫Bar的小伙。最初他倆素未謀面:
有一天小伙Bar去網吧打撸,碰巧Foo也坐在旁邊撸。Bar覺得Foo很漂亮,感覺喜歡上了Foo。於是他邀請Foo一起撸並趁機要了Foo妹子的微信,並說以後帶你飛。其實Bar是為了觀察Foo妹子的票圈,隨時點贊評論,說不定還有機會趁機表白。於是世界因為Bar的艷遇而運行了一段代碼:
- (void)registerAsObserver { /* 當girlFoo的wechat屬性改變時通知boyBar */ [girlFoo addObserver:boyBar forKeyPath:@"wechat" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL]; }
然後在地球上,他倆關系就變了:(畫風陡變,前方高能= =)
Foo因為被Bar關注,所以她一定不再是原來那個美女,而是和以前不一樣的美女(note:還是繼承了原來美女的屬性和方法)。她還是擁有原來的容顏和生活習慣,但是現在她在微信上的一舉一動都會被Bar所察覺到了。
現在女孩回了家,發了一條微信,內容是“哎,好想找個伴”。在最後
[self didChangeValueForKey:@"wechat"];
執行結束的時候小伙就會看到這條消息。
你看KVO的實現方式不是很符合邏輯嗎。
有興趣深入蘋果實現方式的同學就看顧神這篇文章吧《如何自己動手實現 KVO》。他的思路是比較官方化的,只是回調用的block。
這裡介紹他代碼裡runtime中關鍵點實現函數原型:
1.新類被創建並繼承自父類:
objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
2.在新類上重寫setXXX方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
3.女孩Foo的isa指向新類
Class object_setClass(id object, Class cls)
進階篇
假如面試官問你KVO是怎麼實現的,你如上回答可以勉強過關,但是你說出下面的點那就能讓他十分滿意了,說什麼?吐槽。
KVO的缺點
AFNetworking作者Mattt Thompson在《Key-Value Observing》中說:
Ask anyone who’s been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa.
缺點1:所有的observe處理都放在一個方法裡
假設我們要觀察一個tableView的contentSize,看上去使用KVO是個不錯的選擇,因為沒有其他方法去獲知這個屬性被改變。Ok,首先,添加觀察者:
[_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
很好,實現屬性被改變的響應:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self configureView]; }
完成!Just kidding.考慮到該方法中可能有其他的observe事務,所以我們要這樣修改:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) { [self configureView]; } }
如果KVO處理的事務繁多,就會造成該方法代碼特別長,並且十分不優雅,我們還沒轍。
缺點2:嚴重依賴於string
KVO嚴重依賴於string,換句話說如果KVO的keyPath如果錯誤編譯器無法查出,比如我們可能會把@“contentSize”寫成@“contentsize”,然後你就只能傻傻的找半個小時bug。
因此我們不得不使用NSStringFromSelector(@selector(contentSize))編譯器就能判斷是否存在這個屬性,並且我們也好修改,但是代碼這麼長,逼死強迫症。
而且,我們也不能用KVO觀察多值路徑,比如我們觀察一個viewController並且想獲得scrollView的contentOffset,我們是不能用這樣的keyPath:scrollview.contentOffset來得到的。
因此上面的代碼又得變成這樣:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) { [self configureView]; } }
缺點3:需要自己處理superclass的observe事務
對於Objective-C,很多時候runtime系統都會自動幫忙處理superClass的方法,比如dealloc。在ARC下,調用子類的dealloc方法,runtime會自動調用父類的dealloc。但是KVO不會,或者說是不行,因為runtime並不知道父類有沒有observe事務,並且由於它是用協議實現的,一次只能觸發一個observeValueForKeyPath:ofObject:change:context:方法,所以如果子類和父類都有observe事務,我們要這樣做:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) { [self configureView]; } else { // 如果我們忘記這句,那麼父類不會收到通知 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
缺點4:多次相同KVO的removeObserve會導致crash
寫過KVO代碼的人都知道,對同一個對象執行兩次removeObserver操作會導致程序crash。
在同一個文件中執行兩次相同的removeObserver屬於粗心,比較容易debug出來;但是跨文件執行兩次相同的removeObserver就不是那麼容易發現了。
我們一般會在dealloc中進行removeObserver操作(這也是Apple所推薦的)。
思考這樣一個場景:一個JZTableView繼承自UITableview,他們都監聽了tableView的contentSize屬性,那麼UItableview和JZTableView的dealloc都會有這句代碼:
- (void)dealloc { ... [_tableView removeObserver:self forKeyPath:@"contentSize" context:NULL]; ... }
那麼當JZTableview的對象釋放時,他和她父類的dealloc都會被調用,兩次相同的removeObserve,自然就Crash了。
還有很多點,不過說出上面4個就差不多了。
那麼如何改進?
首先列舉缺點:
所有的observe處理都放在一個方法裡
嚴重依賴於string
需要自己處理superclass的observe事務
多次相同KVO的removeObserve會導致crash
然後直接上肖哥的一篇博客:一句代碼,更加優雅的調用KVO和通知
note:以下內容有興趣讀源碼的同學可以看看,否則直接看下一小節吧。
他的代碼架構圖如下:
每個被觀察的對象有一個自己的字典,key為被觀察的keyPath,存的值為一個真正的觀察者對象Target,即dict[keyPath] = target。一個keyPath對應一個target,每個target有一個KVOBlockSet存放該keyPath下的所有Block。
//監聽_objA的name屬性 [_objA xw_addObserverBlockForKeyPath:@"name" block:^(id obj, id oldVal, id newVal) { NSLog(@"kvo,修改name為%@", newVal); }];
缺陷處理結果分析:
所有的observe處理都放在一個方法裡。解決。因為回調被分散成塊了,不再集中。顧神也是用的塊實現的回調。但是用塊也不是很統一,但個人覺得還是比原生的好點。
嚴重依賴於string。未解決。
需要自己處理superclass的observe事務。解決。所有執行了xw_addObserverBlockForKeyPath的塊都會被放入KVOBlockSet,對應的keyPath值被改變時會回調對應的所有block,不存在調用super的問題。
多次相同KVO的removeObserve會導致crash。作者用的hook dealloc調劑remove方法達到自動移除功能,目前來看邏輯沒問題。
THE END
上文說道Foo妹紙發了一條微信“哎,好想找個伴”。Bar小伙見時機成熟,微信上大膽表白“你看我風流倜傥,隨手一敲就是一個十位數內計算器App,你看我做你的鴛鴦咋樣?”,Foo妹紙沉默了10分鐘回答道:“對不起,我可能更喜歡前端工程師”。
- (void)resignObserver { [girlFoo removeObserver:boyBar forKeyPath:@"wechat"]; }
本文作者:伯樂在線 - iosxxoo