天天看點

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

轉載本文需注明出處:微信公衆号EAWorld,違者必究。

引言:

無論用哪種語言進行軟體開發,我們都會接觸到設計模式,個人認為設計模式存在的意義在于:在某些需求下,采用适合的設計模式,使代碼結構合理,進而提高代碼的可讀性、可擴充性、可移植性,此文将要讨論的是OC開發中的一種常用模式之一:觀察者模式之KVO。

KVO俗稱鍵值觀察(key-value observe),鍵值觀察是當被觀察的對象屬性發生改變時,會通知到觀察對象的一種機制。

目錄:

1、KVO的作用

2、KVO的使用方法

3、KVO的實作原理

4、KVO與KVC、代理、通知的差別

5、KVO實作過程中的注意事項

無論用哪種語言進行軟體開發,我們都會接觸到設計模式,個人認為設計模式存在的意義在于:在某些需求下,采用适合的設計模式,使代碼結構合理,進而提高代碼的可讀性、可擴充性、可移植性,此文将要讨論的是iOS開發中的一種常用模式之一:觀察者模式之KVO。我們先看下官方文檔給的KVO介紹:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

翻譯過來就是:KVO是運用isa混寫技術實作自動觀察鍵值的。isa指針是指向對象的類,本質上是指向類中的方法實作。當一個對象注冊觀察者時,這個對象的isa指針被修改指向一個中間類。永遠不要用isa來判斷一個類的繼承關系,而是應該用class方法來判斷類的執行個體。

KVO俗稱鍵值觀察(key-value observe),鍵值觀察是當被觀察的對象屬性發生改變時,會通知到觀察對象的一種機制。

1.KVO的作用

1、監聽帶有狀态的基礎控件,如開關、按鈕等;

2、監聽字元串的改變,當監聽的字元串改變時,來做一些自定義的操作;

3、當資料模型的資料發生改變時,視圖元件能動态的更新,及時顯示資料模型更新後的資料,比如tableview中資料發生變化進行重新整理清單操作,監聽 scrollView的contentOffset屬性監聽頁面的滑動.

2.KVO的使用方法

KVO的使用可分為自動監聽和手動監聽。

1.自動監聽

1.1自動監聽操作步驟:

(1)添加觀察者

(2)在觀察者中添加觀察鍵值方法

(3)在dealloc中移除監聽

1.2示例代碼:

建立兩個類ModelA和ModelB,兩個類中都添加屬性“des”,在控制器中,将B添加為A的觀察者。代碼如下:

ModelA中代碼:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項
ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

ModelB中代碼:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項
ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

控制器中代碼:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

控制器中添加觀察者的方法調用的是如下的類方法:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context           

各個參數說明:

@param observer 被監聽的對象

@param keyPath 被監聽對象的屬性名,不可為空,為空崩潰

@param options 有4種

(1)NSKeyValueObservingOptionNew 把更改之前的值提供給處理方法

(2)NSKeyValueObservingOptionOld 把更改之後的值提供給處理方法

(3)NSKeyValueObservingOptionInitial 把初始化的值提供給處理方法,一旦注冊,立馬就會調用一次。通常它會帶有新值,而不會帶有舊值。

(4)NSKeyValueObservingOptionPrior 分2次調用。在值改變之前和值改變之後

@param context 上下文

上述示例代碼的運作結果如下所示:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

2.手動監聽

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

意思就是說:當某些需要控制監聽過程的場景下,就需要手動監聽,比如:為了盡量減少不必要的觸發通知操作,或者當多個更改同時具備的時候才調用屬性改變的監聽方法。

實作手動監聽的要點主要包括這幾部分:

a.重寫

(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key           

b.在set方法中在指派的前後分别調用

willChangeValueForKey和didChangeValueForKey           

2.1實作部分屬性的手動監聽

在animal.h中添加兩個屬性age和name,在animal.m中關閉age的自動監聽功能,其它屬性依然可以自動監聽,在控制其中實作添加按鈕點選按鈕的時候改變age的值,并觸發監聽方法,代碼如下:

animal類:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

要實作類方法 automaticallyNotifiesObserversForKey,并在其中設定對特定的 key 不自動發送通知(傳回 NO 即可)。這裡要注意,對其它非手動實作的 key,要轉交給 super 來處理[1,2,3]。

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

控制器:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項
ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項
ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

當不點選按鈕的時候,列印結果隻列印了name屬性的值:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

當點選按鈕之後,會手動觸發監聽,列印結果如下:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

2.2所有屬性都手動監聽(禁止自動監聽)

如果需要禁用該類KVO的話直接automaticallyNotifiesObserversForKey傳回NO。

将animal.m中的類方法修改之後:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

運作之後不點選按鈕的話,age和name屬性都不會自動調用監聽方法:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

點選了按鈕之後,隻有實作了手動監聽的age屬性調用了監聽方法:

ios 通過kvc修改屬性會觸發kvo_OC觀察者模式之KVO的使用與思考引言:目錄:1.KVO的作用2.KVO的使用方法3.KVO的實作原理4.KVO與KVC、代理、通知的差別5.KVO實作過程中的注意事項

3.KVO的實作原理

當某一個類的執行個體第一次使用KVO的時候,系統就會在運作期間動态的建立該類的一個派生類,該類的命名規則一般是以NSKVONotifying為字首,以原本的類名為字尾。并且将原型的對象的isa指針指向該派生類。同時在派生類中重載了使用KVO的屬性的setter方法,在重載的setter方法中實作真正的通知機制,正如前面我們手動實作KVO一樣。這麼做是基于設定屬性會調用setter方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設定方式來變更屬性值,如果僅是直接修改屬性對應的成員變量,是無法實作 KVO 的[4,5]。

4.KVO與KVC、代理、通知的差別

1.與KVC的不同?

KVC,即是指 NSKeyValueCoding,一個非正式的 Protocol,提供一種機制來間接通路對象的屬性,而不是通過調用Setter、Getter方法等 顯式的存取方式去通路。KVO 就是基于 KVC 實作的關鍵技術之一。

KVO,即Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改後,對象就會接受到通知。

2.與delegate的不同?

和delegate一樣,KVO和NSNotification的作用都是類與類之間的通信。但是與delegate不同的是:這兩個都是負責發送接收通知,剩下的事情由系統處理,是以不用傳回值;而delegate 則需要通信的對象通過變量(代理)聯系;delegate隻是一對一,而這兩個可以一對多。delegate是非常嚴格的文法,需要定義很多代碼。

3.和notification的差別?

notification比KVO多了發送通知的一步。兩者都是一對多,但是對象之間直接的互動,notification明顯得多,需要notificationCenter來做為中間互動。而KVO如我們介紹的,設定觀察者->處理屬性變化,至于中間通知這一環,則隐秘多了,隻留一句“交由系統通知”,具體的可參照以上實作過程的剖析。notification的優點是監聽不局限于屬性的變化,還可以對多種多樣的狀态變化進行監聽,監聽範圍廣,例如鍵盤、前背景等系統通知的使用也更顯靈活友善[6,7]。

5.KVO實作過程中的注意事項

iOS 10以下會有這些情況,iOS11不會出現這些情況,但是為了代碼的嚴謹性,以及以防出現無法預知的錯誤,還是避開這些比較好。

1、添加觀察者次數與remove次數不比對導緻程式崩潰

連續對同一屬性添加觀察者是可以的,但是也要保證在移除觀察者的時候也要移除對應次,不然可能會引發崩潰(iOS11以上不會崩潰)。

當對同一個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[8]。

2、移除不存在的觀察者(iOS11以上不會崩潰)

當某個對象并沒有添加觀察者時,卻執行了移除觀察者的操作,也會導緻程式崩潰,此處不附相關代碼。

3、被觀察者銷毀時還存在觀察者(iOS11以上不會崩潰)

這種情況常出現在複雜邏輯下,觀察者先于被觀察者銷毀[9]

4、KVO 行為是同步的,并且發生與所觀察的值發生變化的同樣的線程上。沒有隊列或者 Run-loop 的處理。手動或者自動調用 -didChange… 會觸發 KVO 通知。

是以,當我們試圖從其他線程改變屬性值的時候我們應當十分小心,除非能确定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來說,我們不推薦把 KVO 和多線程混起來。如果我們要用多個隊列和線程,我們不應該在它們互相之間用 KVO[10]。

參考資料:

[1]https://www.jianshu.com/p/d447660bed7e

[2]https://www.jianshu.com/p/91c41292b5b9

[3]https://www.jianshu.com/p/5a1c58aacb23

[4]https://www.cnblogs.com/yang-shuai/p/8556326.html

[5]https://www.cnblogs.com/PSSSCode/p/5506577.html

[6]http://www.mamicode.com/info-detail-515516.html

[7]https://www.songma.com/news/txtlist_i38955v.html

[8]https://www.cnblogs.com/wengzilin/p/4346775.html

[9]https://segmentfault.com/a/1190000016896055

[10]https://www.jianshu.com/p/b9f020a8b4c9

關于作者:小幸運,iOS軟體開發工程師,參與普元Dev用戶端OC代碼的維護及新功能開發;使用普元移動開發平台開發郵政移動平台項目郵我行app。熱愛網際網路技術,努力拓寬技術面的程式媛一枚。

關于EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享。