【iOS基礎】KVC / KVO詳解

weixin_34037977發表於2018-12-04

KVC(Key-value coding)

KVC是一種基於NSKeyValueCoding非正式協議的機制,能讓我們直接使用一個或一串字串識別符號去訪問、操作類的屬性。KVO 就是基於 KVC 實現的關鍵技術之一。

KVC基本使用

KVC主要對三種型別進行操作,基礎資料型別及常量、物件型別、集合型別。

- (nullable id)valueForKey:(NSString *)key;

- (void)setValue:(nullable id)value forKey:(NSString *)key;

//keyPath為屬性的路徑 比如xx.xx
- (nullable id)valueForKeyPath:(NSString *)keyPath;

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

//獲取屬性值時,如果屬性不存在則執行該方法;
預設實現方式為丟擲NSUnknownKeyException異常,可重寫這個函式做錯誤處理
- (nullable id)valueForUndefinedKey:(NSString *)key;

//設定屬性值,如果屬性不存在則執行該方法,預設實現方式為丟擲NSUnknownKeyException異常
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

// 對非類物件屬性設定nil時呼叫,預設丟擲異常。
-(void)setNilValueForKey:(NSString *)key

KVC執行機制

  • setValue:forKey:搜尋方式
    1、首先搜尋setKey:方法。(key指成員變數名,首字母大寫)
    2、如果上面的setter方法沒找到,KVC機制會檢查accessInstanceVariablesDirectly類方法有沒有返回YES(預設返回YES)。如果返回YES,那麼按 _key,_isKey,key,iskey的順序搜尋成員變數並賦值;如果返回NO,那麼在這一步KVC會執行setValue:forUndefinedKey:方法。(如果開發者想讓這個類禁用KVC裡,那麼重寫accessInstanceVariablesDirectly類方法讓其返回NO即可)
    3、如果沒有找到成員變數,執行setValue:forUnderfinedKey:方法。

  • valueForKey:的搜尋方式
    1、首先按getKey,key,isKey的順序查詢getter方法,找到直接呼叫。如果是BOOL、int等值型別,會做NSNumber的轉換。
    2、如果上面的getter沒找到,查詢countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外兩個方法中的一個找到,那麼就會返回一個可以響應NSArray所有方法的代理集合的NSArray訊息方法。
    3、還沒找到,查詢countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果這三個方法都找到,那麼就返回一個可以響應NSSet所有方法的代理集合。
    4、還是沒找到,如果類方法accessInstanceVariablesDirectly返回YES。那麼按 _key,_isKey,key,iskey的順序搜尋成員名。
    5、再沒找到,呼叫valueForUndefinedKey方法。

KVC實現分析

KVC運用了isa-swizzing技術。isa-swizzing就是型別混合指標機制。KVC通過isa-swizzing實現其內部查詢定位。isa指標指向維護分發表的物件的類,該分發表實際上包含了指向實現類中的方法的指標和其他資料。

每個類都有一張方法表,是一個hash表,值是函式指標IMP,SEL的名稱就是查表時所用的鍵。
SEL資料型別:查詢方法表時所用的鍵。定義成char*,實質上可以理解成int值。
IMP資料型別:他其實就是一個編譯器內部實現時候的函式指標。當Objective-C編譯器去處理實現一個方法的時候,就會指向一個IMP物件,這個物件是C語言表述的型別。

KVO(Key - Value - Observer)

KVO是Objective-C對觀察者設計模式的一種實現,是一種基於NSKeyValueObserving非正式協議的機制,Cocoa通過這個協議為所有遵守協議的物件提供了一種自動化的屬性觀察能力。指定一個被觀察物件(如A類),當物件中的某個屬性發生變化的時候,物件就會接收到通知,並作出相應的處理。在 MVC 設計架構下的專案,KVO 機制很適合實現 mode 模型和 view 檢視之間的通訊。

KVO實現原理

KVO 的實現依賴於 Objective-C 強大的 Runtime,Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。

  • 建立子類
    當觀察某物件A時,系統會在執行期動態的建立一個名為:NSKVONotifying_A的派生類,該類繼承自物件A的本類。
  • 重寫Setter方法
    在派生類中重寫基類中任何被觀察屬性的setter方法。派生類在被重寫的 setter 方法中實現真正的通知機制。
  • 修改isa指標
    同時派生類還重寫了 class 方法以“欺騙”外部呼叫者它就是起初的那個類。然後系統將這個物件的 isa 指標指向這個動態生成的派生類。

⚠️蘋果為什麼要用子類監聽setter方法,而不用分類呢?原因當你用分類監聽setter方法的時候,原來類中setter方法就不會走了,這樣不好,所以蘋果使用了子類監聽setter方法。

KVO基本使用

1、註冊觀察者

通過addObserver:forKeyPath:options:context:方法註冊觀察者,觀察者可以接收keypath屬性的變化事件。

options引數是一些配置選項,用來指明通知發出的時機和通知響應方法observeValueForKeyPath:ofObject:change:context:的change字典中包含哪些值:
NSKeyValueObservingOptionNew:change字典中應該包含改變後的新值。
NSKeyValueObservingOptionOld:change字典中應該包含改變前的舊值。
NSKeyValueObservingOptionInitial:在註冊觀察者訊息發出後立即傳送通知一次,值改變的時候也會傳送通知
NSKeyValueObservingOptionPrior:分別在值修改前後傳送通知(即一次修改有兩次觸發)

⚠️在呼叫addObserver方法後,KVO並不會對觀察者進行強引用,所以需要注意觀察者的生命週期,在適當的時候將觀察者移除,否則會導致觀察者被釋放帶來的Crash

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2、監聽回撥

在觀察者中實現observeValueForKeyPath:ofObject:change:context:方法,當keypath屬性發生變化後,KVO會回撥這個方法來通知觀察者。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
3、移除觀察者

當觀察者不需要監聽時,可以呼叫removeObserver:forKeyPath:方法將觀察者移除,注意在觀察者消失之前移除否者會導致crash。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

KVO觸發模式

自動監聽

KVO預設自動監聽。
使用 KVO 機制的前提是遵循 KVO 的屬性設定方式來變更屬性值。
例如是否執行了 setter 方法、或者是否使用了 KVC 賦值。
如果賦值沒有通過 setter 方法或者 KVC,而是直接修改屬性對應的成員變數,例如:僅呼叫 ```_name = @"newName",這時是不會觸發 KVO 機制,更加不會呼叫回撥方法的。
KVO無法監聽物件的成員變數。

手動觸發

1)重寫automaticallyNotifiesObserversForKey類方法修改觸發模式,返回YES表示自動,返回NO表示手動,可根據key值定製屬性的觸發模式。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

2)在值改變前後分別呼叫:willChangeValueForKey:didChangeValueForKey:方法.
⚠️KVO觸不觸發與你的值改不改變沒有關係,與willChangeValueForKey:didChangeValueForKey:方法有沒有呼叫有關係。

KVO屬性依賴

當一個屬性與有限個屬性關聯時需要建立屬性依賴。
比如:dog屬性依賴於Dog物件下的name和age屬性。
方法一:重寫keyPathsForValuesAffectingValueForKey:方法

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keypaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        //Dog下面有兩個屬性:name,age
        keypaths = [[NSSet alloc] initWithObjects:@"_dog.name",@"_dog.age", nil];
    }
    return keypaths;
}

方法二:用以下的命名格式實現一個類方法keyPathsForValuesAffecting<Key>,<Key>是屬性的名字(第一個字母要大寫)。

+ (NSSet<NSString *> *)keyPathsForValuesAffectingDog
{
    return [NSSet setWithObjects:@"_dog.name",@"_dog.age", nil];
}

⚠️當你在category裡增加了一個計算屬性時,你不能重寫keyPathsForValuesAffectingValueForKey:方法,因為你不能在category裡重寫方法.在這種情況下,你可以用keyPathsForValuesAffecting<Key>的類方法來實現這個機制。

比較

1.KVC 與 KVO 的不同?

KVC(鍵值編碼),即 Key-Value Coding,基於NSKeyValueCoding非正式協議的機制,使用字串(鍵)訪問一個物件例項變數的機制。而不是通過呼叫 Setter、Getter 方法等顯式的存取方式去訪問。
KVO(鍵值監聽),即 Key-Value Observing,基於NSKeyValueObserving非正式協議的機制,當指定的物件的屬性被修改後,物件就會接受到通知,前提是執行了 setter 方法、或者使用了 KVC 賦值。

2.KVO和、NSNotification(通知)和delegate的區別?

兩者都是觀察者模式。
KVO是被觀察者直接傳送訊息給觀察者,是物件間的互動,而通知則是觀察者和被觀察者通過通知中心物件之間進行互動,即訊息由被觀察者傳送到通知中心物件,再由中心物件發給觀察者,兩者之間並不進行直接的互動。

NSNotification 的優點是監聽不侷限於屬性的變化,還可以對多種多樣的狀態變化進行監聽,監聽範圍廣,例如鍵盤、前後臺等系統通知的使用也更顯靈活方便。

delegate 一般是一對一,而這兩個可以一對多。

參考

KVC官方文件
KVO官方文件
iOS開發技巧系列---詳解KVC
KVO、Delegate、Notification 區別及相關使用場景

相關文章