iOS面試題庫——KVC與KVO
KVC與KVO
本文將會詳解在面試中的常客——KVO實現的原理,在瞭解KVO之前我們要對KVC進行一個全面的瞭解,畢竟連官方文件都提到過:
important: In order to understand key-value observing, you must first understand key-value coding.
1.1KVC
KVC全稱:Key-value coding(鍵-值編碼),通過KVC機制我們可以間接的訪問物件的屬性。而KVC之所以能夠訪問屬性是因為物件遵守了一個非正式的NSKeyValueCoding
協議(NSObject開始就遵守了此協議,所以繼承自NSObject的物件都可以使用KVC)。開發中我們都知道在對於屬性可以使用getter
setter
或者直接使用例項變數來進行直接訪問和修改。但這些訪問方式是需要依靠屬性的get
方法、set
方法、變數名。隨著物件定義的屬性增加或者變動。編譯器生成的這些getter
、setter
會越來越多。KVC則是通過是用字串的名字Key來對屬性進行訪問和修改。
KVC中最關鍵的兩個方法:
-valueForKey:
-setValue:forKey:
1.1.1 valueForKey:
-valueForKey:
是通過\來獲取屬性的值。在一個物件例項中按get<Key>
、<key>
、is<Key>
、_<key>
順序匹配。命中的Value的型別如果是物件直接返回。如果命中的Value是能被包裝成NSNumber的數值型別。包裝成NSNumber返回。不支援NSNumber的數值型別則包裝成NSValue返回。如果沒用命中呼叫-valueForUndefinedKey:
valueForUndefinedKey:
可在子類中重寫忽略丟擲的異常,自己處理。
@interface Person
@property (nonatomic, assign) CGFloat height;
@end
p.height = 119.0;
NSNumber *height = [p valueForKey:@"height"]; //CGFloat 包裝成NSNumber。
注意
- 上述的查詢過程中省略了很多其他情況下的查詢
countOf<Key>
,objectIn<Key>AtIndex:
,countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>:
,有興趣的同學可以去Search Pattern for the Basic Getter檢視詳細的查詢路徑。
1.1.2 setValue:forKey:
-setValue:forKey:
同-valueForKey:
也是根據給定的\匹配方法名set<Key>:
、_set<Key>
,如果命中,呼叫方法將Value作為引數傳入。未命中則看+ accessInstanceVariablesDirectly
是否返回YES,YES則按 _<key>
, _is<Key>
, <key>
, is<Key>
順序匹配例項變數。如果命中直接將Value給變數賦值。NO則呼叫-setValue:forUndefinedKey:
丟擲異常。-setValue:forUndefinedKey:
也可以被子類重寫。
// setValue: forKey:使用
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
1.2 KVO
1.2.1 使用
KVO
在iOS中是觀察者模式的一種表現。我們可以使用KVO
讓某個物件成為另外一個物件的監聽者。當被監聽物件的屬性發生改變時,KVO
就會通知監聽者。
關於KVO
的使用網上有很多教程,KVO使用主要是三個步驟:
- 呼叫
addObserver:forKeyPath:options:context:
註冊成為監聽者。 - 監聽者實現
observeValueForKey:ofObject:change:context:
方法。 - 呼叫
removeObserver:forKeyPath:
移除監聽
前面說過KVO
的實現是建立在KVC
的基礎上的。即被監聽的屬性必須能滿足KVC
的,才能是用KVO
來監聽。
KVO的自動觸發監聽通知的方法系列:
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
KVO的手動觸發監聽通知:
// 關閉balance的自動觸發
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實現手動觸發監聽,你要執行在變更前執行willChangeValueForKey:
方法,在變更後執行didChangeValueForKey:
方法:
- (void)setBalance:(double)balance {
[self willChangeValueForKey:@"balance"];
_balance = balance;
[self didChangeValueForKey:@"balance"];
}
1.2.2 原理
>
Automatic key-value observing is implemented using a technique called isa-swizzling.
檢視NSObject.h檔案。
@interface NSObject <NSObject> {
Class isa;
}
control + command
點選Class
,看到Class
實際上是一個指向obj_class結構體的指標。
typedef struct objc_class *Class;
struct objc_class {
struct objc_class * isa; // 原始的程式碼 Class isa;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到在objc_class
結構體中還有一個isa,即類中還有一個指向objc_class
的指標。類的isa指向的是元類(metaClass
)的。如果將物件看成是通過類的例項。那麼類就是元類的例項咯?
。
這篇文章中我們僅瞭解什麼是Class isa
。objc_class
結構體中的其它部分會單開一篇runtime
的文章來寫。
typedef struct objc_object *id;
struct objc_object {
struct objc_class * isa; // 原始的程式碼 Class isa;
};
id
在obj
中可以代表個物件。id又是一個objc_object
結構體的指標,且objc_object
結構體中的isa
指向的是該物件的類。
在這裡是時候祭出火遍runtime
界的圖: .
回到KVO
。前文提到的isa-swizzling
對於瞭解過runtime
的同學可能對此有點眼熟。method swizzling也是runtime
的一種黑魔法。可以通過method swizzling
進行方法的互換。回到KVO
上面,我們猜測isa-swizzling
就是類似method swizzling
,只不過是對Class的交換。
在KVO中當一個監聽者被註冊被監聽的物件上時。被監聽物件的isa
指標已經被更改了。被監聽物件的isa
指標被修改為指向為一箇中間類。改中間類可能是該類的子類。重寫了被監聽物件的屬性。然後在改屬性值被修改時,會觸發監聽通知。
KVO
的詳細流程
- 監聽者呼叫監聽的方法。
- 被監聽者派生一箇中間類。被監聽物件的
isa
指標指向派生類 - 被監聽的屬性發生變化,由中間類觸發監聽通知(具體方式未知)。
- 監聽者收到通知。觸發
observeValueForKey:ofObject:change:context: