源碼學習
- iOS底層學習:KVO 底層原理
- KVO
- 注冊 KVO 監聽
- 實現 KVO 監聽
- 移除 KVO 監聽
- 處理變更通知
- 手動KVO(禁用KVO)
- 關閉自動通知
- 手動實現 setter 方法
- KVO 和線程
- 如果 KVO 是多線程的
- **單線程的保證**
- 如果沒有 prior 選項
- **prior 選項的作用**
- KVO 實現原理
- 派生類重寫的方法
- 驗證 isa 指向示例
- KVO 注意事項
- 問題總結
iOS底層學習:KVO 底層原理
KVO
KVO 的全稱是 KeyValueObserving,俗稱 “鍵值監聽 ",可以用于監聽某個對象屬性值的改變;KVO 可以通過監聽 key,來獲得 value 的變化,用來在對象之間監聽狀態變化。
基本思想:對目標對象的某屬性添加觀察,當該屬性發生變化時,通過觸發觀察者對象實現的 KVO 接口方法,來自動的通知觀察者。
KVO 是蘋果提供的一套事件通知機制。KVO 和 NSNotificationCenter 都是 iOS 中觀察者模式的一種實現,區別是:NSNotificationCenter 可以是一對多的關系,而 KVO 是一對一的;
注冊 KVO 監聽
通過[addObserver:forKeyPath:options:context:]
方法注冊 KVO,這樣可以接收到 keyPath 屬性的變化事件:
-
observer
:觀察者,監聽屬性變化的對象。該對象必須實現observeValueForKeyPath:ofObject:change:context:
方法。 -
keyPath
:要觀察的屬性名稱,需與屬性聲明的名稱一致。 -
options
:回調方法中收到被觀察者的屬性的舊值或新值等,對 KVO 機制進行配置,修改 KVO 通知的時機以及通知的內容。 -
context
:上下文,會傳遞到觀察者的函數中,用于區分消息,應當為不同值。
options所包括的內容:
-
NSKeyValueObservingOptionNew
:change 字典包括改變后的值。 -
NSKeyValueObservingOptionOld
:change 字典包括改變前的值。 -
NSKeyValueObservingOptionInitial
:注冊后立刻觸發 KVO 通知。 -
NSKeyValueObservingOptionPrior
:值改變前是否也要通知(決定是否在改變前、改變后通知兩次)。
實現 KVO 監聽
通過方法[observeValueForKeyPath:ofObject:change:context:]
實現 KVO 的監聽:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
-
keyPath
:被觀察對象的屬性。 -
object
:被觀察的對象。 -
change
:字典,存放相關的值,根據options
傳入的枚舉返回新值、舊值。 -
context
:注冊觀察者時傳遞的context
值。
移除 KVO 監聽
通過方法[removeObserver:forKeyPath:]
移除監聽;
處理變更通知
每當監聽的 keyPath 發生變化時,會在observeValueForKeyPath
函數中回調
- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(void *)context
change
字典保存了變更信息,具體內容取決于注冊時的NSKeyValueObservingOptions
。
手動KVO(禁用KVO)
KVO 的實現是在注冊的 keyPath 的 setter 方法中,自動插入并調用了兩個函數:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
手動實現 KVO 需先關閉自動生成 KVO 通知,再手動調用通知方法,可靈活添加判斷條件。
關閉自動通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"age"]) { return NO; } else { return [super automaticallyNotifiesObserversForKey:key]; }
}
手動實現 setter 方法
接著手動實現屬性的 setter 方法,在setter方法中先調用willChangeValueForKey:
接著進行賦值操作,然后調用didChangeValueForKey:
- (void)setAge:(int)theAge { [self willChangeValueForKey:@"age"]; age = theAge; [self didChangeValueForKey:@"age"];
}
KVO 和線程
-
KVO 行為是同步的,在所觀察的值發生變化的同一線程上觸發,無隊列或 Runloop 處理。
-
手動或自動調用
didChangeValueForKey:
會觸發 KVO 通知。 -
單線程保證(如主隊列):
-
確保所有監聽某一屬性的觀察者在 setter 方法返回前被通知到。
-
若鍵觀察時附上
NSKeyValueObservingOptionPrior
選項,直到observeValueForKeyPath
被調用前,監聽的屬性返回值不變。- 該鍵對應的值是一個
NSNumber
(BOOL
類型),用于判斷當前 KVO 通知是在屬性值 變更前(前置通知,值為YES
)還是 變更后(后置通知,值為NO
)發送。
- 該鍵對應的值是一個
上述兩個特點可以有效解決復雜場景下的數據一致性和時序問題
我們看以下代碼:
// User.h
@interface User : NSObject
@property (nonatomic, assign) NSInteger age;
@end// ViewController.m
- (void)viewDidLoad {[super viewDidLoad];// 注冊 KVO 監聽[self.user addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];// 主線程修改 agedispatch_async(dispatch_get_main_queue(), ^{self.user.age = 20;NSLog(@"主線程修改 age 為 20");});// 子線程同時修改 agedispatch_async(dispatch_get_global_queue(0, 0), ^{self.user.age = 30;NSLog(@"子線程修改 age 為 30");});
}// KVO 回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {if ([keyPath isEqualToString:@"age"]) {NSNumber *newAge = change[NSKeyValueChangeNewKey];NSLog(@"KVO 接收到 age 變化: %@", newAge);}
}
如果 KVO 是多線程的
- 可能出現通知丟失:主線程和子線程同時修改
age
,觀察者可能只收到最后一次通知(如只收到 30,丟失 20)。 - 可能出現通知順序錯亂:觀察者先收到 30 的通知,再收到 20 的通知,導致邏輯混亂。
單線程的保證
- 原子性:KVO 會在 setter 方法返回前同步且順序地通知所有觀察者,確保:
- 所有觀察者都能收到每一次變化。
- 通知順序與 setter 調用順序一致(先收到 20,再收到 30)。
再來學習NSKeyValueObservingOptionPrior
。該屬性主要應用在復雜數據更新與 UI 動畫同步
// 注冊 KVO,帶上 prior 選項
[self.dataSource addObserver:self forKeyPath:@"items" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior) context:nil];// KVO 回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {if ([keyPath isEqualToString:@"items"]) {// 1. 先收到 prior 通知(change[NSKeyValueChangeNotificationIsPriorKey] = @YES)if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {// 準備動畫(此時 items 還是舊值)[self.tableView beginUpdates];} // 2. 再收到實際變化通知else {// 執行動畫(此時 items 已是新值)[self.tableView endUpdates]; // 自動觸發 insert/delete 動畫}}
}
如果沒有 prior 選項
- 直接在
endUpdates
時才知道數據變化,無法提前準備動畫。 - 可能導致 UI 閃爍或動畫不連貫。
prior 選項的作用
- 分階段通知:
- 第一次通知:在屬性值實際變更前觸發(
NSKeyValueChangeNotificationIsPriorKey = @YES
),此時屬性值仍為舊值。 - 第二次通知:在屬性值變更后觸發(默認行為),此時屬性值已更新。
- 第一次通知:在屬性值實際變更前觸發(
- 實際應用:
- 在第一次通知時,計算新舊數據的差異(如哪些行需要插入 / 刪除)。
- 在第二次通知時,執行
beginUpdates/endUpdates
,讓表格視圖平滑過渡。
KVO 實現原理
KVO 通過isa-swizzling實現,基本流程如下:
isa-swizzling 的本質:
修改對象的類型:通過修改對象的
isa
指針,使其指向另一個類,從而改變對象的行為。
-
創建派生類:編譯器自動為被觀察對象創建派生類(如
NSKVONotifying_XXX
),將被觀察實例的isa
指向該派生類,派生類的superclass
指向原類。 -
重寫方法:若注冊了某屬性的觀察,派生類會重寫該屬性的 setter 方法,并添加通知代碼。
-
消息傳遞:Objective-C 通過
isa
指針找到對象所屬類,調用派生類重寫后的方法,觸發通知。
派生類重寫的方法
- setter 方法:插入
willChangeValueForKey:
和didChangeValueForKey:
調用,觸發通知。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
然后在 didChangeValueForKey:
中,去調用:
- (void)observeValueForKeyPath:(nullable NSString *)keyPathofObject:(nullable id)objectchange:(nullable NSDictionary<NSKeyValueChangeKey, id> *)changecontext:(nullable void *)context;
- class 方法:返回原類,隱藏子類存在,避免
isKindOfClass
判斷異常。
- (Class)class { return class_getSuperclass(object_getClass(self));
}
-
dealloc 方法:釋放 KVO 相關資源。
-
_isKVOA 方法:返回
YES
,標識該類為 KVO 生成的子類。
驗證 isa 指向示例
#import <Foundation/Foundation.h>
#import <objc/runtime.h> @interface ObjectA: NSObject
@property (nonatomic) NSInteger age;
@end @implementation ObjectA
@end @interface ObjectB: NSObject
@end @implementation ObjectB
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"%@", change);
}
@end int main(int argc, const char * argv[]) { @autoreleasepool { ObjectA *objA = [[ObjectA alloc] init]; ObjectB *objB = [[ObjectB alloc] init]; [objA addObserver:objB forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; NSLog(@"%@", [objA class]); // 輸出:ObjectA(表面類型) NSLog(@"%@", object_getClass(objA)); // 輸出:NSKVONotifying_ObjectA(實際類型) } return 0;
}
-
class
方法返回對象所屬的類(原類)。 -
object_getClass
返回對象的isa
指向的實際類(派生類)。
KVO 注意事項
- 內存管理:
-
addObserver
與removeObserver
需成對調用,避免觀察者釋放后仍接收通知導致 Crash。 -
KVO 不對觀察者強引用,需注意觀察者生命周期。否則會導致觀察者被釋放帶來的Crash。
-
方法實現:觀察者必須實現
observeValueForKeyPath:ofObject:change:context:
方法,否則崩潰。 -
KeyPath 安全:在調用KVO時需要傳入一個keyPath,由于keyPath是字符串的形式,所以其對應的屬性發生改變后,字符串沒有改變容易導致Crash。我們可以利用系統的反射機制將keyPath反射出來,這樣編譯器可以在@selector()中進行合法性檢查。
-
數組監聽:默認僅監聽數組對象本身變化,需通過
mutableArrayValueForKey
操作數組或手動觸發通知來監聽元素變化。
問題總結
-
**直接修改成員變量是否觸發 KVO?**不會。KVO 本質是替換 setter 方法,僅通過 setter 或 KVC 修改屬性值時觸發。
-
**KVC 修改屬性會觸發 KVO 嗎?**會。
setValue:forKey:
會調用willChangeValueForKey
和didChangeValueForKey
,觸發監聽器回調。 -
如何監聽數組元素變化?
-
使用
NSMutableArray
并通過mutableArrayValueForKey
獲取數組,其操作會自動觸發通知。 -
手動調用
willChangeValueForKey
和didChangeValueForKey
觸發通知。
當數組中的元素發生變化時,手動觸發KVO通知即可實現監聽。具體實現方式如下:
使用NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
選項來監聽可變數組中的元素變化。這兩個選項會在KVO通知中包含新舊值的信息,因此可以在觀察者中獲取到數組中元素的變化。
代碼如下:
[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//觀察者中實現.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {if ([keyPath isEqualToString:@"myArray"]) {NSArray *oldArray = change[NSKeyValueChangeOldKey];NSArray *newArray = change[NSKeyValueChangeNewKey];// 處理數組元素的變化}
}