詳解KVO 一、基本概念 二、基本用法 三、高級用法 四、關於NSKVONotifying_原類型名(簡稱此類爲中間類)

一、基本概念

        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,則需要手動觸發
}
  1. 在VC的Tap事件中設置斷點,並執行p *self.child命令,可以看出child的類型是MyTestClass


  2. 註冊觀察者——此時需要通過調用automaticallyNotifiesObserversForKey方法以獲取是否是自動觸發的信息


  3. 自動修改對象類型——把child由MyTestClass修改爲NSKVONotifying_爲前綴的NSKVONotifying_MyTestClass類型



  1. 修改被觀察者的屬性值


  2. 通知觀察者


2.4 梳理與總結:

  1. 添加觀察者時,系統把對象的類型修改爲NSKVONotifying_原類型名,這個新類繼承自原類型
  2. 添加觀察者時,系統調用automaticallyNotifiesObserversForKey,詢問是否自動觸發被觀察者
  3. 修改被觀察者的屬性
  • 調用willChangeValueForKey:
  • 調用原來類的setter方法([super setAge:age])
  • 調用didChangeValueForKey:
  • didChangeValueForKey:內部會調用observer的observerValueForKeyPath:ofObject:change:context:方法
  1. 觀察者獲得通知

三、高級用法

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

--------

  1. _isKVOA:是一個辨識碼,來判斷這個類是不是因爲KVO產生的動態子類
  2. dealloc:判斷它是否進行釋放
  3. class:是類的信息
  4. setMyCustomType:是要變化的屬性的setter方法
  5. 在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進行某些處理,那麼可能會忽略此錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章