1. 程式人生 > >iOS面試題庫——KVC與KVO

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方法、變數名。隨著物件定義的屬性增加或者變動。編譯器生成的這些gettersetter會越來越多。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。

注意

  1. 上述的查詢過程中省略了很多其他情況下的查詢 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使用主要是三個步驟:

  1. 呼叫addObserver:forKeyPath:options:context:註冊成為監聽者。
  2. 監聽者實現observeValueForKey:ofObject:change:context:方法。
  3. 呼叫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 isaobjc_class結構體中的其它部分會單開一篇runtime的文章來寫。

typedef struct objc_object *id;

struct objc_object {
    struct objc_class * isa;  // 原始的程式碼 Class isa;
};

idobj中可以代表個物件。id又是一個objc_object結構體的指標,且objc_object結構體中的isa指向的是該物件的類。

在這裡是時候祭出火遍runtime界的圖: isa & super class.

回到KVO。前文提到的isa-swizzling對於瞭解過runtime的同學可能對此有點眼熟。method swizzling也是runtime的一種黑魔法。可以通過method swizzling進行方法的互換。回到KVO上面,我們猜測isa-swizzling就是類似method swizzling,只不過是對Class的交換。

在KVO中當一個監聽者被註冊被監聽的物件上時。被監聽物件的isa指標已經被更改了。被監聽物件的isa指標被修改為指向為一箇中間類。改中間類可能是該類的子類。重寫了被監聽物件的屬性。然後在改屬性值被修改時,會觸發監聽通知。

KVO的詳細流程

  1. 監聽者呼叫監聽的方法。
  2. 被監聽者派生一箇中間類。被監聽物件的isa指標指向派生類
  3. 被監聽的屬性發生變化,由中間類觸發監聽通知(具體方式未知)。
  4. 監聽者收到通知。觸發observeValueForKey:ofObject:change:context: