一、基本概念
KVO的全稱是Key-value observing(鍵值觀察),它提供了一種機制,允許對象在其他對象的特定屬性發生變化時收到通知。
二、基本用法
// MyTestClass.h
@interface MyTestClassBase : NSObject
@end
@protocol ATextProtocol <NSObject>
- (void)notImpFun;
@end
@interface MyTestClass : MyTestClassBase <ATextProtocol>
- (void)printClassObject;
+ (void)printClassClass;
@property (nonatomic, assign) BOOL childProperty;
@end
// MyTestClass.m
#import "MyTestClass.h"
#import <objc/runtime.h>
@implementation MyTestClassBase
@end
@implementation MyTestClass
- (void)printClassObject {
NSLog(@"%@ printClassObject", NSStringFromClass(self.class));
Class cls = self.class;
while (cls != Nil) {
NSLog(@"object =%@ class =%@", self, NSStringFromClass(cls));
cls = class_getSuperclass(cls);
}
}
+ (void)printClassClass {
NSLog(@"MyTestClass printClassClass");
Class cls = self;
while (cls != Nil) {
NSLog(@"class =%@", NSStringFromClass(cls));
cls = class_getSuperclass(cls);
}
}
- (void)setChildProperty:(BOOL)childProperty {
_childProperty = childProperty;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
Class cls = self;
while (cls != Nil) {
NSLog(@"resolveInstanceMethod class=%@ sel=%@", NSStringFromClass(cls), NSStringFromSelector(sel));
cls = class_getSuperclass(cls);
}
return [super resolveInstanceMethod:sel];
}
@end
// ViewController.m
@interface ViewController ()
@property (nonatomic, strong) MyTestClass *child;
@property (nonatomic, strong) UIButton *button;
@property (nonatomic, assign) BOOL tap;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_child = [MyTestClass new];
[_child printClassObject];
[MyTestClass printClassClass];
_button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 120, 60)];
[_button setTitle:@"添加KVO" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[_button addTarget:self action:@selector(tap:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_button];
}
- (void)tap:(id)sender {
[_child addObserver:self forKeyPath:@"childProperty" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
_child.childProperty = YES;
_tap = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"childProperty"]) {
NSLog(@"%@", change);
}
[_child printClassObject];
[MyTestClass printClassClass];
// [_child notImpFun];
}
- (void)dealloc {
if (_tap) {
[_child removeObserver:self forKeyPath:@"childProperty"];
}
}
2.1 基本用法
// 首先需要通過被觀察對象調用addObserver,把觀察者添加到被觀察對象上
// NSKeyValueObservingOptions 指定了能夠獲取值的類型
// context是觀察者與被觀察之之間通信的上下文,其用於複雜場景
[_child addObserver:self forKeyPath:@"childProperty" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 然後觀察者對象實現observeValueForKeyPath即可
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
2.2 移除觀察者
雖然在iOS9之後KVO就不需要手動移除,但是推薦還是手動移除(如果覺得Apple原生的KVO使用麻煩——確實聽麻煩的,可以使用一些三方庫,他們一版會自動移除觀察者)。
An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
以上是官方的說法,大意是:解除分配時,觀察者不會自動刪除自己。 被觀察的對象繼續發送通知,而忽略了觀察者的狀態。 但是,發送到已釋放對象的更改通知與任何其他消息一樣,會觸發內存訪問異常。 因此,您要確保觀察者在從內存中消失之前將自己移除。
2.3 如何觸發observeValueForKeyPath(這裏是初步解釋,後續有詳細說明)
其實很簡單,iOS運行時在背後做了一些手腳。爲了更清楚觀察具體觸發過程,請添加如下代碼。
// 在MyTestClass.m中爲MyTestClass添加如下代碼
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
NSLog(@"automaticallyNotifiesObserversForKey");
return YES; // 這裏返回YES,則KVO會自動觸發,如果返回NO,則需要手動觸發
}
-
在VC的Tap事件中設置斷點,並執行p *self.child命令,可以看出child的類型是MyTestClass
-
註冊觀察者——此時需要通過調用automaticallyNotifiesObserversForKey方法以獲取是否是自動觸發的信息
-
自動修改對象類型——把child由MyTestClass修改爲NSKVONotifying_爲前綴的NSKVONotifying_MyTestClass類型
-
修改被觀察者的屬性值
-
通知觀察者
2.4 梳理與總結:
- 添加觀察者時,系統把對象的類型修改爲NSKVONotifying_原類型名,這個新類繼承自原類型
- 添加觀察者時,系統調用automaticallyNotifiesObserversForKey,詢問是否自動觸發被觀察者
- 修改被觀察者的屬性
- 調用willChangeValueForKey:
- 調用原來類的setter方法([super setAge:age])
- 調用didChangeValueForKey:
- didChangeValueForKey:內部會調用observer的observerValueForKeyPath:ofObject:change:context:方法
- 觀察者獲得通知
三、高級用法
3.1 手動觸發觀察者
[_child willChangeValueForKey:@"childProperty"];
_child.childProperty = YES;
[_child didChangeValueForKey:@"childProperty"];
3.2 觀察多個屬性
當有時某個屬性依賴於其他多個屬性時,對當前屬性的觀察相當於需要觀察多個屬性時,如何處理?參加以下代碼。
// MyTestClass.h,爲MyTestClass添加以下屬性
@property (nonatomic, assign) NSNumber *mainProperty; // 被觀察的屬性
@property (nonatomic, assign) NSNumber *subProperty1; // 被mainProperty 依賴
@property (nonatomic, assign) NSNumber *subProperty2; // 被mainProperty 依賴
// MyTestClass.m,添加以下方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"mainProperty"]) {
NSArray *affectingKeys = @[@"subProperty1", @"subProperty2"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSNumber *)mainProperty {
return [NSNumber numberWithInteger:_subProperty1.integerValue + _subProperty2.integerValue];
}
// ViewController.m,觀察child的mainProperty
- (void)tap:(id)sender {
[_child addObserver:self
forKeyPath:@"mainProperty"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
_child.subProperty1 = @1;
_child.subProperty2 = @2;
_tap = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"mainProperty"]) {
NSLog(@"%@", change);
}
[_child printClassObject];
[MyTestClass printClassClass];
// [_child notImpFun];
}
/ * _child.subProperty2 = @1; 的輸出結果
2021-12-01 10:35:44.693231+0800 StudyKVO[13682:158801] {
kind = 1;
new = 1;
old = 0;
}
*/
/ * _child.subProperty2 = @2; 的輸出結果
2021-12-01 10:35:44.693231+0800 StudyKVO[13682:158801] {
kind = 1;
new = 3;
old = 1;
}
*/
3.3 觀察集合類型
// MyTestClass.h
@property (nonatomic, strong) NSMutableArray *dataArray;
// MyTestClass.m
- (NSMutableArray*)dataArray {
if (!_dataArray) {
_dataArray = [NSMutableArray array];
}
return _dataArray;
}
// ViewController.m
- (void)tap:(id)sender {
[_child addObserver:self
forKeyPath:@"dataArray"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[[self.child mutableArrayValueForKey:@"dataArray"] addObject:@1];
_tap = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) {
NSLog(@"%@", change);
}
[_child printClassObject];
[MyTestClass printClassClass];
// [_child notImpFun];
}
四、關於NSKVONotifying_原類型名(簡稱此類爲中間類)
4.1 基本內容
被觀察者的對象的isa指針執行了一個運行時自動生成的類NSKVONotifying_原類型名,此類繼承自被觀察者的原始類型。下面對此類進行分析。
2021-12-01 11:25:59.921673+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun setMyCustomType: --- imp 0x7fff207a3203
2021-12-01 11:25:59.921824+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun class --- imp 0x7fff207a1d0d
2021-12-01 11:25:59.921939+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun dealloc --- imp 0x7fff207a1abd
2021-12-01 11:25:59.922053+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun _isKVOA --- imp 0x7fff207a1ab5
2021-12-01 11:26:00.373625+0800 StudyKVO[16739:209645] ----------
2021-12-01 11:26:01.282659+0800 StudyKVO[16739:209645] cls MyTestClass : fun printClassObject --- imp 0x102476830
2021-12-01 11:26:01.282837+0800 StudyKVO[16739:209645] cls MyTestClass : fun setChildProperty: --- imp 0x1024769f0
2021-12-01 11:26:01.282958+0800 StudyKVO[16739:209645] cls MyTestClass : fun mainProperty --- imp 0x102476c50
2021-12-01 11:26:01.283111+0800 StudyKVO[16739:209645] cls MyTestClass : fun childProperty --- imp 0x102476cd0
2021-12-01 11:26:01.283262+0800 StudyKVO[16739:209645] cls MyTestClass : fun setMainProperty: --- imp 0x102476d00
2021-12-01 11:26:01.283395+0800 StudyKVO[16739:209645] cls MyTestClass : fun subProperty1 --- imp 0x102476d30
2021-12-01 11:26:01.283551+0800 StudyKVO[16739:209645] cls MyTestClass : fun setSubProperty1: --- imp 0x102476d50
2021-12-01 11:26:01.283651+0800 StudyKVO[16739:209645] cls MyTestClass : fun subProperty2 --- imp 0x102476d80
2021-12-01 11:26:01.283735+0800 StudyKVO[16739:209645] cls MyTestClass : fun setSubProperty2: --- imp 0x102476da0
2021-12-01 11:26:01.283814+0800 StudyKVO[16739:209645] cls MyTestClass : fun myCustomType --- imp 0x102476e10
2021-12-01 11:26:01.283906+0800 StudyKVO[16739:209645] cls MyTestClass : fun setMyCustomType: --- imp 0x102476e30
2021-12-01 11:26:01.284014+0800 StudyKVO[16739:209645] cls MyTestClass : fun .cxx_destruct --- imp 0x102476e70
2021-12-01 11:26:01.284093+0800 StudyKVO[16739:209645] cls MyTestClass : fun dataArray --- imp 0x1024768f0
2021-12-01 11:26:01.290066+0800 StudyKVO[16739:209645] cls MyTestClass : fun setDataArray: --- imp 0x102476dd0
--------
- _isKVOA:是一個辨識碼,來判斷這個類是不是因爲KVO產生的動態子類
- dealloc:判斷它是否進行釋放
- class:是類的信息
- setMyCustomType:是要變化的屬性的setter方法
- 在dealloc中移除觀察者後,對象的isa就變回原有類型
4.2 總結分析
- 在添加觀察時,runtime會產生一箇中間類:
- 中間類繼承於原類
- 中間類會重寫被觀察key的setter方法,
- 對象的isa從指向元類,變成指向中間類
- 當對屬性賦值時,對象會根據isa找到中間類對應的setter方法,然後在willChangeValueForKey和didChangeValueForKey方法之間進行賦值,進而觸發-(void)observeValueForKeyPath:ofObject:change:context:方法。
- 當在dealloc中移除通知後,isa會重新指向原來的類,相關實例變量的值不變。dealloc後中間類並不會釋放,依然在註冊類中。
4.3 最後的說明
如果你那第二節中的代碼進行測試,就會發現在即使在添加觀察之後,在VC的代碼中po _child.class依然輸出的是MyTestClass,但是在MyTestClass的resolveInstanceMethod方法中,獲取的class確是NSKVONotifying_MyTestClass。如果你基於類名MyTestClass,在resolveInstanceMethod進行某些處理,那麼可能會忽略此錯誤。