Important: In order to understand key-value observing, you must first understand key-value coding. -- 官方文檔html
想要理解KVO,必須先理解KVC,由於鍵值觀察是創建在鍵值編碼 的基礎上。觀衆老爺們能夠參考筆者的上篇文章iOS進階之路 (十三)KVC。ios
Key-value observing
is a mechanism that allows objects to be notified of changes to specified properties of other objects.git
KVO (Key-value observing)
是一個非正式協議,容許對象在其餘對象的指定屬性發生更改時獲得通知。iOS開發者可使用KVO
來檢測對象屬性的變化、快速作出響應,這可以爲咱們在開發強交互、響應式應用以及實現視圖和模型的雙向綁定時提供大量的幫助。github
KVO’s primary benefit is that you don’t have to implement your own scheme to send notifications every time a property changes. Its well-defined infrastructure has framework-level support that makes it easy to adopt—typically you do not have to add any code to your project. In addition, the infrastructure is already full-featured, which makes it easy to support multiple observers for a single property, as well as dependent values.objective-c
KVO
的主要好處是,沒必要在每次屬性更改時都實現本身的方案來發送通知。這個過程大部分是內建的,自動的,透明的。這使得采用它很容易,一般您沒必要向項目中添加任何代碼。此外,KVO支持單個屬性添加多個觀察者以及依賴值。設計模式
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
複製代碼
observer
: 註冊 KVO 通知的對象。觀察者必須實現 key-value observing
方法 - observeValueForKeyPath:ofObject:change:context:
。keyPath
: 被觀察者的屬性的 keypath
,相對於接受者,值不能是 nil。options
: 表明 NSKeyValueObservingOptions
的位掩碼,它指定了觀察通知中包含了什麼context
:在 observeValueForKeyPath:ofObject:change:context:
傳給 observer
參數的上下文傳字符串作爲 keypath 比直接使用屬性更糟糕,由於任何錯字或者拼寫錯誤都不會被編譯器察覺,最終致使不能正常工做。 一個聰明的解決方案是使用 NSStringFromSelector
和一個 @selector
字面值:數組
NSStringFromSelector(@selector(isFinished))
複製代碼
由於 @selector
檢查目標中的全部可用 selector
,這並不能阻止全部的錯誤,但它能夠用來捕獲大部分改變。安全
關於context,蘋果官方文檔也作了精彩的註釋。bash
The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.網絡
context
中包含着將在相應的更改通知中傳遞迴觀察員的任意數據。您能夠指定NULL並徹底依賴於keyPath
肯定更改通知的來源。可是這種方法可能出現問題:尤爲是處理那些繼承自同一個父類的子類,而且這些子類有相同的 keypath
。
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
一種更安全、更可擴展的方法是:使用context
來確保接收到的通知是發送給觀察者的,而不是發送給超類。 如何設置一個好的 content
呢?蘋果官方文檔也給出了推薦的方法。
// a Person instance registers itself as an observer for an Account instance’s
// 大致意思就是:一個靜態變量存着它本身的指針。這意味着它本身什麼也沒有。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
複製代碼
蘋果官方文檔推薦在 init
或 viewDidLoad
add觀察者,在 dealloc
裏移除觀察者, 保證 add
和 remove
是成對出現的。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
複製代碼
調用 –removeObserver:forKeyPath:context:
時, 當這個對象沒有被註冊爲觀察者(由於它已經解註冊了或者開始沒有註冊),會拋出一個異常。有意思的是,沒有一個內建的方式來檢查對象是否註冊。 蘋果官方文檔推薦使用: @try
/ @catch
移除觀察者。
- (void)dealloc {
@try {
// 3. unsubscribe
[_account removeObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
context:ContentSizeContext];
}
@catch (NSException *exception) {
}
}
複製代碼
// key 方法
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
// keyPath方法:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
複製代碼
例子:
To use KVO, first you must ensure that the observed object, the Account in this case, is KVO compliant. Typically, 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. KVO Compliance describes the difference between automatic and manual key-value observing, and how to implement both.
要使用KVO
,首先必須確保被觀察的對象符合KVO
。一般,若是您的對象繼承自NSObject
,而且您以一般的方式建立屬性,那麼您的對象及其屬性將自動與KVO
兼容。固然,也能夠手動實現聽從性。KVO Compliance
講述述了自動和手動鍵值觀察之間的區別,以及如何實現二者。
能夠經過複寫 automaticallyNotifiesObserversForKey:
的返回值,選擇自動退出 KVO。同時該類方法還能完成控制特定屬性的目的。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
複製代碼
// 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 能夠幫助咱們將多個屬性值的更改合併成一個,這樣在回調的時候就有一次了,同時也能最大程度地減小處於應用程序特定緣由而致使的通知發生。
willChangeValueForKey
,在更改值以後調用didChangeValueForKey
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
複製代碼
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
複製代碼
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
複製代碼
NSKeyValueChange
( NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或 NSKeyValueChangeReplacement
),受影響的對象的索引用 NSIndexSet
對象- (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"];
}
複製代碼
有一些屬性的值取決於一個或者多個其餘對象的屬性值,一旦某個被依賴的屬性值變了,依賴它的屬性的變化也須要被通知。
要自動觸發 To-One
關係,有兩種方法:
keyPathsForValuesAffectingValueForKey:
方法keyPathsForValuesAffecting<Key>
的方法。舉個例子: 一我的的全名 fullName 是由 firstName 和 lastName 組成的,一個觀察 fullName 的程序在 firstName 或者 lastName 變化時也應該接收到通知。
- (NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
複製代碼
keyPathsForValuesAffectingValueForKey:
方法, 來代表 fullname 屬性是依賴於 firstname 和 lastname 的:+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray: affectingKeys];
}
return keyPaths;
}
複製代碼
至關於在影響 fullName 值的 keypath 中新加了兩個key:lastName 和 firstName,很容易理解。
值得注意的是:須要先對父類發送 keyPathsForValuesAffectingValueForKey
消息,以避免干擾父類中對此方法的重寫
+ (NSSet *)keyPathsForValuesAffectingFullName
{
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
複製代碼
若是在分類中,使用 keyPathsForValuesAffectingFullName
更合理,由於分類中是不容許重載方法的,因此 keyPathsForValuesAffectingValueForKey
方法確定是不能在分類中使用的。
keyPathsForValuesAffectingValueForKey:
方法不支持包含 to-many
關係的 keypath
。
好比,有一個 Department(部門)
類,它有一個針對 Employee(僱員)
類的 to-many 關係,Employee
類有 salary(薪資)
屬性。你但願 Department
類有一個 totalSalary
屬性來計算全部員工的薪水,也就是 Department的totalSalary
依賴於全部 Employee的salary
屬性。你不能經過實現 keyPathsForValuesAffectingTotalSalary
方法並返回 employees.salary
。
你能夠用KVO將parent
(好比Department)做爲全部children
(好比Employee)相關屬性的觀察者。你必須在把child添加或刪除到parent時也把parent做爲child的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:
方法中咱們能夠針對被依賴項的變動來更新依賴項的值:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
複製代碼
將 Department 實例對象註冊爲觀察者,而後觀察對象爲 totalSalary 屬性,可是在通知回調中會手動調用 totalSalary 屬性的 setter 方法,而且傳入值是經過 KVC 的集合運算符的方式取出 employees 屬性所對應的集合中全部 sum 值之和。而後在 totalSalary 屬性的 setter 方法中,會相應的調用 willChangeValueForKey: 和 didChangeValueForKey: 方法。
先看看官方文檔的解釋
Automatic key-value observing is implemented using a technique called isa-swizzling.
isa-swizzling
。The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
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
指針所指向的內容發生了變化,指向了一箇中間類而不是真正的類。這也致使 isa
指針並不必定是指向實例所屬的真正的類。You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
isa
指針來肯定類成員身份。相反,你應該使用 class
方法來肯定對象實例所屬的類。根據官網文檔,咱們初步判斷,在 KVO 底層會有一箇中間類生成,中間類會讓對象的 isa 指針發生變化。測試一下:
self.person
的isa 指向 AKPerson類
self.person
的isa 指向 NSKVONotifying_AKPerson類
結論1:添加觀察者後,系統動態生成了中間類
NSKVONotifying_AKPerson
,實例對象self.person
的isa由指向原始AKPerson類
, 修改成指向中間類NSKVONotifying_AKPerson
。
那麼這個中間類和原來的類是什麼關係呢? 遍歷下二者的類以及子類,測試一下
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls {
// 註冊類的總數
int count = objc_getClassList(NULL, 0);
// 建立一個數組, 其中包含給定對象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取全部已註冊的類
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
複製代碼
結論2:添加觀察者以後,中間類 NSKVONotifying_AKPerson 是原始類
AKPerson
的子類.
KVO關注的是屬性值的變化,而 屬性 = 成員變量 + getter + setter
,顯然只有 setter 和成員變量賦值兩種方式能夠改變屬性值。
咱們作下測試:AKPerson 添加成員變量name
和 屬性nickName
, 添加觀察者後同時修改二者的值:
@interface AKPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
複製代碼
結論3: 動態子類觀察的是setter方法
咱們能夠遍歷原始類和中間類的方法列表, 觀察下中間類重寫了哪些方法?
#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"***** %@ *****", cls);
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
複製代碼
AKPerson類
中的方法沒有改變(imp實現地址沒有變化)NSKVONotifying_AKPerson中間類
重寫了 父類AKPerson
的 dealloc方法
NSKVONotifying_AKPerson中間類
重寫了 基類NSObject
的 class方法
和 _isKVOA方法
_isKVOA
方法:作個標識class方法
仍然是返回的是原始類,目的就是隱藏中間類的存在,讓調用者調用 class方法
結果先後一致NSKVONotifying_AKPerson中間類
指會 AKPerson類
經過observationInfo
命令,能夠在 lldb 裏查看一個被觀察對象的全部觀察信息。
KVO 是 Cocoa 爲咱們提供的一種模式,用於訂閱其餘對象屬性的更改。 KVO 很強大,沒錯。知道它內部實現,或許能幫助更好地使用它,或在它出錯時更方便調試。但官方 KVO 提供的 API 並很差用。甚至咱們的平常開發中,除非萬不得已,不多去使用官方 KVO 的 API。
在 Soroush Khanlou 的 Key-Value Observing Done Right 文章中,KVO 被批判的體無完膚。好比:
KVO all comes through one method
: 只能經過重寫 -observeValueForKeyPath:ofObject:change:context: 方法來得到通知,想要提供自定義的 selector ,想要傳一個 block ,沒門!KVO is string-ly typed
: 傳遞的 keyPath 是字符串類型的,任何錯字或者拼寫錯誤都不會被編譯器察覺,最終致使不能正常工做。KVO requires you to handle superclasses yourself
: KVO要求咱們本身處理超類的問題。尤爲是處理那些繼承自同一個父類的子類,而且這些子類有相同的 keypath。KVO can crash when deregistering
:KVO 須要手動移除,移除的時候還可能引發崩潰,並且 KVO 沒有一個內建的方式來檢查對象是否註冊或銷燬。例如:一個超類也觀察同一個對象上的同一個參數,會發生什麼狀況?它會被移除兩次,第二次會致使崩潰。若是想完美的解決這些問題,建議閱讀下強大的自定義KVO框架 facebookarchive/KVOController
在 Objective-C
和 Cocoa
中,有許多事件之間進行通訊的方式,而且每一個都有不一樣程度的形式和耦合
NSNotification
:Key-Value Observing
:Delegate
Callbacks
最後附上KVO的流程圖: