網站首頁 編程語言 正文
正文
Key-Value Observing(KVO) 是一種機制,它允許對象在其他對象的指定屬性發生更改時得到通知。要使用 KVO,首先你必須確保被觀察對象是 KVO 兼容的。通常,如果你的對象繼承自 NSObject 并且你以通常的方式創建屬性,那么你的對象及其屬性將自動兼容 KVO。
KVO 的主要好處是你不必實現自己的方案來在每次屬性更改時發送通知,其定義良好的基礎架構具有框架級別的支持。
KVO 的基本使用
你必須執行以下步驟以使對象能夠接收 KVO 兼容屬性的 KVO 通知:
- 使用方法
addObserver:forKeyPath:options:context:
將觀察者注冊到被觀察對象。 - 在觀察者內部實現
observeValueForKeyPath:ofObject:change:context:
以接受更改通知消息。 - 使用
removeObserver:forKeyPath:
方法取消注冊觀察者,當它不再應該接收消息時。至少,在觀察者從內存中釋放之前調用此方法。
注冊為觀察者
被觀察對象首先通過發送 addObserver:forKeyPath:options:context:
消息將觀察者注冊到被觀察對象,并將觀察者和要觀察的屬性的 keyPath
傳遞。觀察者還指定了一個選項參數和一個上下文指針來管理通知的各個方面。
Options
options
參數為常量選項的按位或,它會影響通知中提供的 change
字典的內容以及生成通知的方式??蛇x的值有如下四個:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) { NSKeyValueObservingOptionNew = 0x01, // 新值 NSKeyValueObservingOptionOld = 0x02, // 舊值 // 屬性的初始值 NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04, // 在變化前發送通知 // change 字典通過包含鍵 NSKeyValueChangeNotificationIsPriorKey 和 NSNumber 包裝 YES 的值來表示更改前通知。 NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08 };
Context
addObserver:forKeyPath:options:context:
消息中的 context
指針包含任意數據,這些數據將在相應的更改通知中傳遞回觀察者。你可以指定 NULL 并完全依賴 keyPath
字符串來確定更改通知的來源,但這種方法可能會導致父類出于不同原因也觀察相同 keyPath
的對象出現問題。
一種更安全、更可擴展的方法是使用上下文來確保你收到的通知是發給你的觀察者而不是父類的。
類中唯一命名的靜態變量的地址是一個很好的上下文。在父類或子類中以類似方式選擇的上下文不太可能重疊。你可以為整個類選擇一個上下文,并依靠通知消息中的 keyPath
字符串來確定發生了什么變化?;蛘?,你可以為每個觀察到的 keyPath
創建一個不同的上下文,這完全繞過了字符串比較的需要,從而提高了通知解析的效率。
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]; }
需要注意的是 addObserver:forKeyPath:options:context:
方法不維護對觀察對象、被觀察對象或 context
的強引用。你應該確保根據需要維護對觀察對象和觀察對象以及 context
的強引用。
接收變更通知
當對象的被觀察屬性的值發生變化時,觀察者會收到一個 observeValueForKeyPath:ofObject:change:context:
消息。所有的觀察者都必須實現這個方法。
被觀察對象提供觸發通知的 keyPath
、被觀察對象、包含有關更改的詳細信息的字典以及在為該 keyPath
注冊觀察者時提供的上下文指針。
change
字典的 NSKeyValueChangeKindKey
提供有關發生的更改類型的信息:
- 如果被觀察對象的值發生了變化,則
NSKeyValueChangeKindKey
對應的值為NSKeyValueChangeSetting
。根據觀察者注冊時指定的選項,change
字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
條目包含更改之前和之后的屬性值。如果屬性是對象,則直接提供值。如果屬性是基本數據類型或C
結構,則將值包裝在NSValue
對象中。 - 如果觀察到的屬性是一對多關系,則
NSKeyValueChangeKindKey
條目還通過分別返回NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
指示關系中的對象是否被插入、刪除或替換。 -
change
字典的NSKeyValueChangeIndexesKey
條目是一個NSIndexSet
對象,用于指定更改的關系中的索引。如果在注冊觀察者時將NSKeyValueObservingOptionNew
或NSKeyValueObservingOptionOld
指定為選項,則change
字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
條目是包含更改前后相關對象值的數組。
- (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]; } }
如果你在注冊觀察者時指定了 NULL 上下文,則將通知的 keyPath
與你正在觀察的 keyPath
進行比較以確定發生了什么變化。如果你對所有觀察到的 keyPath
使用單個上下文,則首先根據通知的 context
對其進行測試,并找到匹配項,然后使用 keyPath
字符串比較來確定具體發生了什么變化。如果你為每個 keyPath
提供了唯一的 context
,如上述代碼所示,一系列簡單的指針比較會同時告訴你通知是否針對此觀察者,如果是,則哪些 keyPath
已更改。
在任何情況下,觀察者應該總是調用父類的 observeValueForKeyPath:ofObject:change:context:
實現,當它不能識別 context
(或者在簡單的情況下,任何 keyPath
),因為這意味著一個父類已經注冊了相關通知。
移除觀察者
你可以通過向被觀察對象發送 removeObserver:forKeyPath:context:
消息來移除觀察者,并指定觀察者、keyPath
和 context
。
- (void)unregisterAsObserverForAccount:(Account*)account { [account removeObserver:self forKeyPath:@"balance" context:PersonAccountBalanceContext]; [account removeObserver:self forKeyPath:@"interestRate" context:PersonAccountInterestRateContext]; }
移除觀察者時,請記住以下幾點:
- 如果尚未注冊為觀察者,則要求將其作為觀察者移除會導致
NSRangeException
。你可以只調用一次removeObserver:forKeyPath:context:
來對應調用addObserver:forKeyPath:options:context:
,或者如果這在你的應用程序中不可行,請將removeObserver:forKeyPath:context:
調用放在try/catch
塊中處理潛在的異常。 - 觀察者在釋放時不會自動移除自己。被觀察對象繼續發送通知,而忽略了觀察者的狀態。但是,與任何其他消息一樣,發送到已釋放對象的更改通知會觸發內存訪問異常。因此,你要確保觀察者在從內存中消失之前將自己移除。
- 該協議無法詢問對象是觀察者還是被觀察者。構建你的代碼以避免發生相關的錯誤。一個典型的模式是在觀察者初始化期間注冊為觀察者(例如在
init
或viewDidLoad
中)并在釋放期間取消注冊(通常在dealloc
中),確保正確配對和有序的添加和刪除消息,并且觀察者在它被內存釋放之前取消注冊觀察。
KVO 的觸發方式
KVO 觸發的方式有兩種:
- 自動觸發:
NSObject
提供了自動觸發KVO
的基本實現。 - 手動觸發:由開發者自行控制哪些屬性會觸發
KVO
自動觸發
// 使用訪問器方法 [account setName:@"Savings"]; // 使用 setValue:forKey:. [account setValue:@"Savings" forKey:@"name"]; // 使用 setValue:forKeyPath: [document setValue:@"Savings" forKeyPath:@"account.name"]; // 使用 mutableArrayValueForKey: 檢索關系代理對象。 Transaction *newTransaction = <#Create a new transaction for the account#>; NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction];
手動觸發
在某些情況下,你可能希望控制通知過程,例如,盡量減少因應用程序特定原因而不必要的觸發通知,或將多個更改組合到單個通知中。手動觸發通知提供了執行此操作的方法。
手動和自動通知并不相互排斥。除了已經存在的自動通知之外,你還可以自由發布手動通知。更典型的是,你可能希望完全控制特定屬性的通知。
在這種情況下,你覆蓋了 automaticNotifiesObserversForKey:
的 NSObject
實現。對于要排除其自動通知的屬性,automaticNotifiesObserversForKey:
的子類實現應該返回 NO
。子類實現應該為任何無法識別的鍵調用 super
。
+ (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)theBalance { [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"]; }
在有序一對多關系的情況下,你不僅必須指定更改的鍵,還必須指定更改的類型和所涉及對象的索引。更改的類型是指定 NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
或 NSKeyValueChangeReplacement
的 NSKeyValueChange
。受影響對象的索引作為 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"]; }
注冊依賴鍵
在很多情況下,一個屬性的值取決于另一個對象中的一個或多個其他屬性的值。如果一個屬性的值發生變化,那么派生屬性的值也應該被標記為變化。如何確保為這些依賴屬性發布 KVO
通知取決于關系的基數。
一對一的關系
要為一對一關系自動觸發通知,你應該覆蓋 keyPathsForValuesAffectingValueForKey:
或實現一個合適的方法,該方法遵循它為注冊相關鍵定義的模式。
例如,一個人的全名取決于名字和姓氏。 返回全名的方法可以寫成如下:
- (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@",firstName, lastName]; }
當 firstName
或 lastName
屬性發生更改時,必須通知觀察 fullName
屬性的應用程序,因為它們會影響屬性的值。
一種解決方案是覆蓋 keyPathsForValuesAffectingValueForKey:
指定一個人的 fullName
屬性依賴于 lastName
和 firstName
屬性。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; }
你的方法覆寫通常應該調用 super
并返回一個集合,該集合包括該集合中的任何成員(以免干擾父類中此方法的覆蓋)。
你還可以通過實現遵循命名約定 keyPathsForValuesAffecting<Key>
的類方法來實現相同的結果,其中 <Key>
是依賴于值的屬性的名稱(首字母大寫)。使用這種模式,上述代碼可以重寫為名為 keyPathsForValuesAffectingFullName
的類方法:
+ (NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"lastName", @"firstName", nil]; }
當你使用 category
將計算屬性添加到現有類時,你不能覆蓋 keyPathsForValuesAffectingValueForKey:
方法,因為你不應該覆蓋類中的方法。在這種情況下,實現一個匹配的 keyPathsForValuesAffecting<Key>
類方法來利用這個機制。
KVO 實現細節
讓我們來首先實現一個簡單的使用了 KVO
的 demo
:
@interface Person : NSObject @property (nonatomic, assign) NSUInteger age; @end @implementation Person @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.person = [Person new]; // 添加監聽 [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 改變對象屬性值 self.person.age = 18; } - (void)dealloc { // 移除監聽 [self.person removeObserver:self forKeyPath:@"age"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { // 被監聽的屬性值改變后的回調 NSLog(@"%@", change); } @end
在 addObserver:forKeyPath:options:context:
一行打上斷點,會發現在添加觀察者的前后,person
對象的 isa
指針的指向發生了變化。
添加觀察者前:
添加觀察者后:
尤其可知,KVO
是通過 runtime
動態生成 NSKVONotify_Person
類的方式,并將 person
對象的 isa
指針指向了新類,來為我們實現 KVO
的,這套技術稱之為 isa-swizzling
。其中,NSKVONotify_Person
類是 Person
類的子類,NSKVONotify_Person
還重寫了 class
方法,用于在我們進行內省時,得到的是當前類的準確類。
再將斷點打在 observeValueForKeyPath:ofObject:change:context:
方法中,可以看到,在接收到通知的過程中,調用了系統實現的 _NSSetUnsignedLongLongValueAndNotify:
方法:
正是通過該方法,實現了更新屬性值+通知觀察者值有變動的功能。該方法也是 Foundation
為我們實現的一系列針對不同類型用于在 KVO
時設值的方法,想了解該方法的底層實現可以看下面這篇文章:
iOS開發KVO實現細節解密
原文鏈接:https://juejin.cn/post/7114601200648978445
相關推薦
- 2023-01-03 Android序列化實現接口Serializable與Parcelable詳解_Android
- 2022-06-17 C語言深入講解內存操作問題_C 語言
- 2022-07-12 springboot整合jasypt加密yml配置文件
- 2024-03-02 【JQuery】Ajax 參數為數組 的方法
- 2022-04-21 R語言繪制帶ErrorBar的分組條形圖代碼的分享_R語言
- 2022-07-15 Android自定義view實現圓形進度條效果_Android
- 2022-03-29 Python函數裝飾器的使用詳解_python
- 2024-03-06 Springboot實現緩存預熱
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支