KVO 从基本使用到原理剖析
文章目錄
- 1. 簡介
- 2. 基本使用
- 2.1 設置觀察者
- 2.2 接收屬性改變消息
- 2.3 移除觀察者
- 2.4 KVO 使用實例
- 3. 原理剖析
- 3.1 KVO 的實現
- 3.2 NSKVONotifying_XXX類探究
- 3.3 NSKVONotifying_XXX 中 setter 方法的實現
- 4. 面試題解析
- 4.1 KVO的是如何實現的?
- 4.2 如何手動觸發KVO?
- 4.3 直接改變成員變量會觸發KVO嗎?
- 參考資料
1. 簡介
KVO 是 Key Value Observe 的縮寫,主要通過為需要監聽的對象屬性設置觀察者,讓觀察者接收到屬性值改變的消息通知,是 iOS 對觀察者模式的一種實現。
2. 基本使用
2.1 設置觀察者
當我們需要這個為某個對象設置觀察者時,可以使用一下方法:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法中各參數的含義:
-
observer:觀察者對象
-
keyPath:需要觀察的屬性在對象中的路徑,相關解釋如下:
// 汽車類 @interface Car : NSObject @property (nonatomic, copy) NSString *brandName; // 汽車品牌名稱 @end// Person 類 @interface Person : NSObject @property (nonatomic, assign) NSInteger age; // 年齡 @property (nonatomic, strong) Car *car; // 持有的汽車對象 @endPerson *p = [Person new]; // 如果要監聽這個人的年齡,keyPath 為 @"age" // 如果要監聽這個人的汽車的品牌名稱,keyPath 為 @"car.brandName" -
options:監聽選項,用于設置屬性改變后需要接收的值。該枚舉類型的定義如下:
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,// 如果指定,則在每次修改屬性時,會在修改通知被發送之前預先發送一條通知給觀察者,與-willChangeValueForKey:被觸發的時間是相對應的。NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08 }; -
context:上下文信息,會隨著監聽到的信息一起傳給觀察者,可以設置為nil。
2.2 接收屬性改變消息
如果某個類成為了觀察者,就需要重寫以下方法。當監聽的屬性值被改變時,方法就會被調用。
/// @param keyPath 發生改變的屬性路徑 /// @param object 對應的觀察者對象 /// @param change 可以根據監聽選項獲取到相應值的字典 /// @param context 在注冊觀察者時傳入的上下文信息 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;2.3 移除觀察者
我們可以通過兩種以下方法移除觀察者。根據 Foundation 框架中的注釋表述,通常我們只要使用方法 2 就可以了。
但是當我們對同一個對象的同一屬性多次添加同一個觀察者,但傳入不同的上下文信息(context)時,我們就必須要使用方法 1 進行移除,同時傳入的上下文信息(context)要與添加時相對應。
// 1 - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));// 2 - (void)removeObserver:(NSObject *)observerforKeyPath:(NSString *)keyPath;注意:addObserver: 和 removeObserver: 必須成對出現,否則會在觀察者被釋放后,因為收到 KVO 監聽的消息導致 Crash。官方建議在 init 方法中添加觀察者,dealloc方法中移除觀察者,以保證它們可以成對出現。
2.4 KVO 使用實例
接下來我們以一個簡單的實例來演示 KVO 的使用。首先創建一個只包含 age 屬性的 Person,用來作為被監聽的對象。
/// Person 類 @interface Person : NSObject @property(nonatomic, assign) NSInteger age; // 年齡屬性 @end在本例中,我們給 person 對象的 age 屬性添加一個監聽器,當用戶點擊屏幕時,會更改 person 對象的 age 屬性的值,進行觸發 KVO。
@interface ViewController () @property (nonatomic, strong) Person *person; // person對象 @end@implementation ViewController- (void)dealloc {// 移除觀察者[self.person removeObserver:self forKeyPath:@"age"]; }- (void)viewDidLoad {[super viewDidLoad];// 初始化,并設置 age 的初始值self.person = [[Person alloc] init];self.person.age = 18;// 監聽選項,這里選擇“原來的值”和“改變后的值”NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;// 為屬性 person 添加觀察者[self.person addObserver:self forKeyPath:@"age" options:options context:nil]; }/// 屏幕的觸摸事件 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// 改變 age 的值self.person.age = 19; }- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"keyPath: %@,\n object: %@,\n change: %@,\n context: %@", keyPath, object, change, context); }@end在點擊屏幕之后,我們可以看到控制臺打印出了以下信息:
可以看到,我們可以根據監聽選項獲取到該屬性改變前和改變后的值。通常我們會根據 keyPath 和 object 來區分對不同屬性的監聽,然后改變的值來進行相應操作。
3. 原理剖析
學習一樣知識,不能總停留在使用的表層,應該深入去學習它內部的實現原理,下面我們就一步步學習 KVO 的實現原理。
3.1 KVO 的實現
我們利用 runtime 提供的 object_getClass() 函數去獲取對象在添加 KVO 監聽前和添加 KVO 監聽后,isa 指針所指向的類對象。
NSLog(@"person 添加KVO監聽之前 - %@", object_getClass(self.person));// 監聽選項,這里選擇“原來的值”和“改變后的值” NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; // 為屬性 person 添加觀察者 [self.person addObserver:self forKeyPath:@"age" options:options context:nil];NSLog(@"person 添加KVO監聽之后 - %@", object_getClass(self.person));程序運行后可以看到控制臺打印的結果:
可以發現,在添加 KVO 監聽前,person 對象的 isa 指針是指向 Person 類對象的;而在添加了 KVO 監聽后,person 對象的 isa 指針是指向了一個叫 NSKVONotifying_Person 的類對象。
這說明,KVO 的實現是通過 runtime API 生成一個叫 NSKVONotifying_XXX 的類,然后將被監聽對象的 isa 指針指向該類對象,以此為基礎來實現的。
3.2 NSKVONotifying_XXX類探究
為了能夠探究 NSKVONotifying_XXX 的作用,我們可以利用下面的方法將它內部的方法列表打印出來。
// 打印類中所有的方法 - (void)printMethodNamesOfClass:(Class)cls {unsigned int outCount;// 獲取方法數組Method *methodList = class_copyMethodList(cls, &outCount);// 存儲方法名NSMutableString *methodNames = [NSMutableString string];// 遍歷所有方法for (int i = 0; i < outCount; i++) {// 獲取方法Method method = methodList[i];// 獲取方法名NSString *methodName = NSStringFromSelector(method_getName(method));[methodNames appendString:methodName];if (i != outCount - 1) {[methodNames appendString:@", "];}}free(methodList); // 需要手動釋放方案列表內存NSLog(@"%@", methodNames); }在添加了 KVO 之后,我們通過下面代碼,打印 NSKVONotifying_XXX 中包含的方法。
[self printMethodNamesOfClass:object_getClass(self.person)];控制臺的打印結果如下,可以看到其中包含 setAge:, class, dealloc 和 _isKVOA 四個方法。
從我們的開發經驗中,我們不難猜測, KVO 是通過在生成的 NSKVONotifying_XXX 類中重寫 setter 方法來實現的。
而對與其他三個方法,這里也給出基于經驗猜測的參考解釋:
-
class 是為了對外屏蔽生成 NSKVONotifying_XXX 類,使用者還是認為自己正在使用的是原來的類。
我們在添加 KVO 之后,利用 class 方法獲取 person 的類,可以發現返回的還是 Person,這也側面驗證了上述的想法。
NSLog(@"person 添加KVO監聽之后 - %@", [self.person class]); -
dealloc 是用于在對象銷毀前進行收尾工作。
-
_isKVO 是為了判斷該類是否為了 KVO 而生成的。
3.3 NSKVONotifying_XXX 中 setter 方法的實現
@implementation Person- (void)willChangeValueForKey:(NSString *)key {NSLog(@"[BEGIN] willChangeValueForKey:");[super willChangeValueForKey:key];NSLog(@"[END] willChangeValueForKey:"); }- (void)didChangeValueForKey:(NSString *)key {NSLog(@"[BEGIN] didChangeValueForKey:");[super didChangeValueForKey:key];NSLog(@"[END] didChangeValueForKey:"); }@end4. 面試題解析
4.1 KVO的是如何實現的?
通過 runtime API 生成 NSKVONotifying_XXX 類,在其中重寫所監聽屬性的 setter 方法,然后將被監聽對象的 isa 指針該類對象。
于是,當被監聽屬性的值通過 setter 方法被修改之后,就會觸發 KVO。
4.2 如何手動觸發KVO?
我們可以通過調用 willChangeValueForKey: 和 didChangeValueForKey: 來觸發 KVO。假如我們在 3.1 的例子中要手動觸發 KVO,可以使用下面的代碼。
[self.person willChangeValueForKey:@"age"]; [self.person didChangeValueForKey:@"age"];4.3 直接改變成員變量會觸發KVO嗎?
@interface Person : NSObject {@publicNSInteger _age; // 成員變量 } @property(nonatomic, assign) NSInteger age; // 屬性 @end我們直接對成員變量 _age 進行修改,是不會觸發 KVO的。因為 KVO 的觸發,需要調用到生成 NSKVONotifying_Person 類中的 setter 方法,直接修改成員變量是不會經過 setter 方法的,所以不會觸發 KVO。
參考資料
- https://www.cnblogs.com/496668219long/p/4470923.html
- https://www.jianshu.com/p/badf5cac0130
看完要記得點贊哦,如果有問題可以提出來,大家交流一下~
總結
以上是生活随笔為你收集整理的KVO 从基本使用到原理剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS - 解决设置导航栏按钮图片变色的
- 下一篇: CocoaPods 的安装与卸载