從官方文檔來武裝一下自己(游擊隊->正規軍)
原理說明
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
- 當對一個類添加觀察後,這個類的 isa 指針被指向了一箇中間類,而非真實的了類
關於 KVO 內部實現的原理,官方也就一句話帶過
而這個中間類和 isa 就是 KVO 的核心了
爲什麼要實現一箇中間類呢?
KVO的核心在於,屬性的改變的時候,可以兼容的到
我們一般在更改屬性值的時候一般都是
- 點方法 -> setter 方法
- setValueForKey
- 直接賦值
對於直接賦值,這個屬於直接修改了指針的指向,這個就很難抓到了
所以重點看 setter 方法 和 setValueForKey 這兩個方法
於是 KVO 就要在運行的時候動態的兼容我們的 setter 方法
那麼蘋果的實現方法就是,運行時新建了一個被觀察的對象的子類
將被觀察對象的 isa 指針指向子類
isa 略作說明
每個對象都有 isa 指針,isa 指針存儲了一個類所有的信息。 例如:所有的方法、所有的屬性 換句話來說,我們對 OC 類的操作,底層都是對 isa 指針的操作 例如: isKindOf 方法 其源碼可以看出 對象 -> isa -> isa_class -> isa_super_class 直到找到 isa 類型相等 沒有找到則 return false,找到則 return true
子類重寫了 setter 方法,然後就可以抓到屬性的改變了
至於怎麼通知給觀察者改變的,看下面的 官方文檔重點翻譯
官方文檔重點翻譯
- OS X 中,model and controller layers 很大程度省依賴 KVO
if your objects inherit from NSObject and you create properties in the usual way, your objects and their properties will automatically be KVO Compliant. It is also possible to implement compliance manually
- 繼承自 NSObject 的類,常規的方式創建的屬性,都自動的可以使用 KVO
Not all classes are KVO-compliant for all properties
- 並不是所有的屬性都兼容 KVO
Manual change notification provides additional control over when notifications are emitted, and requires additional coding. You can control automatic notifications for properties of your subclass by implementing the class method [automaticallyNotifiesObserversForKey:](https://developer.apple.com/documentation/objectivec/nsobject/1409370-automaticallynotifiesobservers)
.
- 重寫子類的 automaticallyNotifiesObserversForKey: 可以來控制屬性的改變是否發送 KVO 通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }
- 觸發 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];
- 重寫 setter 方法 發送更改通知
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; }
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; // Remove the transaction objects at the specified indexes. [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; }