你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 青少年一定要讀的KVO指南

青少年一定要讀的KVO指南

編輯:IOS開發基礎

1459909145132027.jpeg

入門篇

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

蘋果的實現方式用猿哥這張經典的圖理解起來還不錯,但是可能不太適合新手。

6941baebjw1f6e3mjj45aj20qh0fvjsc.jpg

KVO的實現是經典的添加中間層的思想,雖然用了isa-swizzling,但是很符合邏輯,並沒有什麼黑魔法。我們來個生活中的實例講解下:

我們假設有一個叫Foo的女孩,有一個叫Bar的小伙。最初他倆素未謀面:

6941baebjw1f6e3mj77gdj20a90bt0sn.jpg

有一天小伙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];
}

然後在地球上,他倆關系就變了:(畫風陡變,前方高能= =)

6941baebjw1f6e3miw3f7j20yg0jzgmr.jpg

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:以下內容有興趣讀源碼的同學可以看看,否則直接看下一小節吧。

他的代碼架構圖如下:

6941baebjw1f6e3mihpwpj20k20ezdg8.jpg

每個被觀察的對象有一個自己的字典,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

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved