KVO的那是些事兒

KVO的定義

???鍵值觀察,對象采用的一種非正式協(xié)議,用于將其他對象的指定屬性的更改通知給對象??梢杂^察到任何對象屬性,包括簡單屬性,一對一關(guān)系和一對多關(guān)系。一對多關(guān)系的觀察者被告知所做更改的類型,以及更改涉及哪些對象。NSObject提供了NSKeyValueObserving協(xié)議的實(shí)現(xiàn),該協(xié)議為所有對象提供了自動觀察功能。開發(fā)者可以通過禁用自動觀察者通知,并使用此協(xié)議中的方法實(shí)施手動通知來進(jìn)一步優(yōu)化通知。

常用方法和參數(shù)

注冊觀察

注冊觀察者
-addObserver:forKeyPath:options:context:

移除觀察者。
-removeObserver:forKeyPath:

在給定上下文的情況下,移除觀察者。
-removeObserver:forKeyPath:context:

變更通知

鍵值發(fā)生改變時,通知觀察對象。
-observeValueForKeyPath:ofObject:change:context:

通知觀察者變更

通知觀察到的對象給定屬性的值即將更改。
-willChangeValueForKey:

通知觀察到的對象給定屬性的值已更改。
-didChangeValueForKey:

通知觀察對象對于指定的有序多對關(guān)系,將在給定的索引處執(zhí)行指定的更改。
-willChange:valuesAtIndexes:forKey:

通知觀察對象指定的多對多關(guān)系在索引上發(fā)生了指定的更改。
-didChange:valuesAtIndexes:forKey:

通知觀察對象即將對指定的無序多對關(guān)系進(jìn)行指定的更改。
-willChangeValueForKey:withSetMutation:usingObjects:

通知觀察對象對指定的無序?qū)Χ嚓P(guān)系進(jìn)行了指定的更改。
-didChangeValueForKey:withSetMutation:usingObjects:

觀察定制

是否允許自動鍵值觀察。
+automaticallyNotifiesObserversForKey:

示例:name屬性不允許自動觀察
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
   
   if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

// 手動觀察name屬性
- (void)setName:(NSString *)name {
    
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

為屬性的值返回一組鍵路徑,觀察多個鍵值時使用,任一鍵值有更新,都會被通知到。
+keyPathsForValuesAffectingValueForKey:

示例:觀察Person的dog屬性中name、age的變化

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) Dog *dog;
@property (nonatomic, strong) NSMutableArray *arr;

@end

@implementation Person

- (instancetype)init {
    self = [super init];
    if (self) {
        _dog = [[Dog alloc] init];
        _arr = [NSMutableArray array];
    }
    return self;
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        
        // 此處一定要帶下劃線
        NSArray *arr = @[@"_dog.name", @"_dog.age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
    }
    return keyPaths;
}
@end

@interface Dog : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

#import "KVOViewController.h"
#import "Person.h"

@interface KVOViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation KVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    Person *person = [Person new];
    _person = person;
    
    // 觀察person內(nèi)dog屬性的name和age變化
//    [_person addObserver:self forKeyPath:@"dog.name" options:(NSKeyValueObservingOptionNew) context:nil];
//    [_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
    [_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
   
    static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
    _person.dog.name = name;
    _person.dog.age = i++;
}
@end

如果給定數(shù)組中指定的任何屬性發(fā)生更改,則將觀察對象配置為發(fā)布給定屬性的更改通知。不推薦使用
+setKeys:triggerChangeNotificationsForDependentKey:

返回一個指針,該指針標(biāo)識有關(guān)向觀察對象注冊的所有觀察者的信息。
observationInfo

參數(shù)

可以觀察到的變化類型。
NSKeyValueChange

NSKeyValueChangeSetting     // 觀察set方法
NSKeyValueChangeInsertion   // 觀察集合 insert 元素操作
NSKeyValueChangeRemoval     // 觀察集合 remove 元素操作
NSKeyValueChangeReplacement // 觀察集合 replace 元素操作

可以在變更字典中返回的值。
NSKeyValueObservingOptions

NSKeyValueObservingOptionOld      // 表示在change字典中包含了改變前的值。
NSKeyValueObservingOptionNew      // 表示在change字典中包含新的值。
NSKeyValueObservingOptionInitial  // 在注冊觀察者的方法return的時候就會發(fā)出一次通知。
NSKeyValueObservingOptionPrior    // 在值發(fā)生改變前、后各發(fā)出一次通知,每次change都會有兩個通知。

可以顯示在更改字典中的鍵。
NSKeyValueChangeKey

NSKeyValueChangeKindKey                 // 指明了變更的類型,值為“NSKeyValueChange”枚舉中的某一個,類型為NSNumber
NSKeyValueChangeNewKey                  // 被監(jiān)聽屬性改變后新值的key,當(dāng)監(jiān)聽屬性為一個集合對象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時,該值返回的是一個數(shù)組,包含插入,替換后的新值(刪除操作不會返回新值)
NSKeyValueChangeOldKey                  // 被監(jiān)聽屬性改變前舊值的key,當(dāng)監(jiān)聽屬性為一個集合對象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時,該值返回的是一個數(shù)組,包含刪除,替換前的舊值(插入操作不會返回舊值)
NSKeyValueChangeIndexesKey              // 如果NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,這個鍵的值是一個NSIndexSet對象,包含了增加,移除或者替換對象的index。
NSKeyValueChangeNotificationIsPriorKey  // 如果注冊監(jiān)聽者是options中指明了NSKeyValueObservingOptionPrior,change字典中就會帶有這個key,值為NSNumber類型的YES.

可以對無序集合進(jìn)行的突變類型。
NSKeyValueSetMutationKind

NSKeyValueUnionSetMutation      // 已將指定集中的觀察者添加到觀察對象。
NSKeyValueMinusSetMutation      // 正在從觀察對象中除去指定集合中的觀察者
NSKeyValueIntersectSetMutation  // 正在從觀察對象中移除不在指定集合中的觀察者。
NSKeyValueSetSetMutation        // 一組觀察者正在替換觀察對象中的現(xiàn)有對象。

KVO的內(nèi)部實(shí)現(xiàn)

???當(dāng)我們對Person對象的name屬性進(jìn)行觀察的時候,實(shí)際上會在-addObserver:forKeyPath:options:context:內(nèi)部動態(tài)創(chuàng)建一個Person的子類 NSKVONotifying_Person,動態(tài)修改觀察對象的類型,觀察子類,然后重寫子類的set方法來實(shí)現(xiàn)。

image.png

image.png

可以發(fā)現(xiàn),在addObserver:forKeyPath:options:context:執(zhí)行前,person的isa指針指向Person,addObserver:forKeyPath:options:context執(zhí)行后,person的isa指針指向了NSKVONotifying_Person,說明在addObserver:forKeyPath:options:context方法內(nèi)部發(fā)生了指針混寫。

自定義KVO的監(jiān)聽

新建一個NSObject的分類NSObject+JRKVO,定義方法:jr_addObserver: forKeyPath: options: context:,在方法實(shí)現(xiàn)中如下代碼:

#import "NSObject+JRKVO.h"
#import <objc/message.h>

static const char *JRKVO_observer = "JRKVO_observer";
static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";

@implementation NSObject (JRKVO)

- (void)jr_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               options:(NSKeyValueObservingOptions)options
               context:(void *)context {
    
    // 創(chuàng)建、注冊子類
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
    
    Class newClass = objc_getClass(newClassName.UTF8String);
    if (!newClass) {
        
        // 創(chuàng)建類
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        // 注冊類
        objc_registerClassPair(newClass);
    }
    
    // setter方法首字母大寫
    NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
    NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
    SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
    
    // 動態(tài)修改監(jiān)聽的類型,監(jiān)聽子類
    object_setClass(self, newClass);
    
    // 子類添加setter方法
    Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
    const char *types = method_getTypeEncoding(getMethod);
    class_addMethod(newClass, setSEL, (IMP)setMethod, types);
    
    // 保存Observer
    objc_setAssociatedObject(self, JRKVO_observer, observer, OBJC_ASSOCIATION_ASSIGN);
    
    // 保存setter、getter方法
    objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

void setMethod(id self, SEL _cmd, id newValue) {
    
    // 獲取setter、getter方法
    NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
    NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);
    
    // 保存子類類型
    Class class = [self class];
    
    // isa指向原類
    object_setClass(self, class_getSuperclass(class));
    
    // 調(diào)用原類getter方法,獲取舊值
    id oldValue = objc_msgSend(self, NSSelectorFromString(getName));
    
    // 調(diào)用原類setter方法
    objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);
    
    id observer = objc_getAssociatedObject(self, JRKVO_observer);
    
    NSMutableDictionary *change = [@{} mutableCopy];
    if (newValue) {
        change[NSKeyValueChangeNewKey] = newValue;
    }
    if (oldValue) {
        change[NSKeyValueChangeOldKey] = oldValue;
    }
    
    // 通知觀察者
    objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), getName, self, change, nil);
    
    // isa重新指向子類
    object_setClass(self, class);
}

@end

調(diào)用jr_addObserver: forKeyPath: options: context:方法,觀察person的name屬性

 [_person jr_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];

改變person的name屬性的值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
   static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
    // _person.name = name;
    objc_msgSend(_person, @selector(setName:), name);
}
image.png

image.png

set方法已經(jīng)執(zhí)行,并且觀察者回調(diào)方法中已經(jīng)輸出name新值、舊值。

自定義KVO的監(jiān)聽 帶block回調(diào)

在上面創(chuàng)建的NSObject分類NSObject+JRKVO中,定義方法:jr_addObserverForKeyPath: options: callbak:,在方法實(shí)現(xiàn)中如下代碼:

#import "NSObject+JRKVO.h"
#import <objc/message.h>

static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";
static const char *JRKVO_callback = "JRKVO_callback";

@implementation NSObject (JRKVO)

- (void)jr_addObserverForKeyPath:(NSString *_Nonnull)keyPath
                         options:(NSKeyValueObservingOptions)options
                         callbak:(JRKVOCallbackBlock _Nonnull )block {
    
    // 創(chuàng)建、注冊子類
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
    
    Class newClass = objc_getClass(newClassName.UTF8String);
    if (!newClass) {
        
        // 創(chuàng)建類
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        // 注冊類
        objc_registerClassPair(newClass);
    }
    
    // setter方法首字母大寫
    NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
    NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
    SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
    
    // 動態(tài)修改監(jiān)聽的類型,監(jiān)聽子類
    object_setClass(self, newClass);
    
    // 子類添加setter方法
    Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
    const char *types = method_getTypeEncoding(getMethod);
    class_addMethod(newClass, setSEL, (IMP)setMethod, types);
    
    // 保存block
    objc_setAssociatedObject(self, JRKVO_callback, block, OBJC_ASSOCIATION_COPY);
    
    // 保存setter、getter方法
    objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

void setMethod(id self, SEL _cmd, id newValue) {

    // 獲取setter、getter方法
    NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
    NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);

    // 保存子類類型
    Class class = [self class];

    // isa指向原類
    object_setClass(self, class_getSuperclass(class));

    // 調(diào)用原類getter方法,獲取舊值
    id oldValue = objc_msgSend(self, NSSelectorFromString(getName));

    // 調(diào)用原類setter方法
    objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);

    NSMutableDictionary *change = [@{} mutableCopy];
    if (newValue) {
        change[NSKeyValueChangeNewKey] = newValue;
    }
    if (oldValue) {
        change[NSKeyValueChangeOldKey] = oldValue;
    }

    // 回調(diào)setter方法值的改變
    JRKVOCallbackBlock callback = objc_getAssociatedObject(self, JRKVO_callback);
    if (callback) {
        callback(change);
    }

    // isa重新指向子類
    object_setClass(self, class);
}
@end

調(diào)用jr_addObserverForKeyPath: options: callbak:方法,觀察person的name屬性變化。

image.png

觀察多個鍵值的變化

keyPathsForValuesAffectingValueForKey:實(shí)現(xiàn)多個鍵值觀察

示例:觀察Person的dog屬性中name、age的變化
Person 類中實(shí)現(xiàn)如下方法

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
   
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        
        // 此處一定要帶下劃線
        NSArray *arr = @[@"_dog.name", @"_dog.age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
    }
    return keyPaths;
}

VC中觀察Person的dog屬性

[_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"dog 改變了%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
    _person.dog.name = name;
    _person.dog.age = i++;
}

點(diǎn)擊屏幕,改變dog對象name、age的值,打印結(jié)果
image.png

通過keyPathsForValuesAffectingValueForKey:把多個keyPath放在一個集合set中,統(tǒng)一觀察,dog的任何一個屬性發(fā)生變化,都會通知到。比起單個keyPath觀察,確實(shí)方便了很多。

數(shù)組的觀察

通過KVC中的mutableArrayValueForKey: 實(shí)現(xiàn)數(shù)組的觀察,數(shù)組中元素改變(增、刪、改)都可以觀察到。

示例代碼:

#import "KVOViewController.h"
#import "Person.h"
#import "NSObject+JRKVO.h"
#import <objc/message.h>

@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end

@implementation KVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    Person *person = [Person new];
    _person = person;
    
    // 觀察 person的arr屬性的變化, 數(shù)組元素的增刪改
    [_person addObserver:self forKeyPath:@"arr" options:(NSKeyValueObservingOptionNew) context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {  
    NSLog(@"Person中的arr改變了%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
   
    // 通過KVC獲取到數(shù)組,
    NSMutableArray *arr = [_person mutableArrayValueForKey:@"arr"];
    
    // add 對應(yīng) NSKeyValueChange 為 NSKeyValueChangeInsertion, 即:kind 為 2
    [arr addObject:name];
   
    // replace 對應(yīng) NSKeyValueChange 為 NSKeyValueChangeReplacement, 即:kind 為 4
    [arr replaceObjectAtIndex:0 withObject:@"1111"];
   
    // remove 對應(yīng) NSKeyValueChange 為 NSKeyValueChangeRemoval, 即:kind 為 3
    [arr removeObjectAtIndex:0];
}
@end

點(diǎn)擊屏幕,操作數(shù)組元素的增、刪、改,打印結(jié)果:

image.png

執(zhí)行完mutableArrayValueForKey:后,觀察arr的類型已經(jīng)變成NSKeyValueMutableArray類型,其實(shí)mutableArrayValueForKey:內(nèi)部動態(tài)創(chuàng)建了NSMutableArray的子類NSKeyValueMutableArray,然后指針混寫指向子類,并返回子類,后續(xù)觀察arr實(shí)際上是觀察子類。

image.png

這里的kind = 2,kind = 3,kind = 4, 分別對應(yīng)NSKeyValueChangeNSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement。也就是說KVO不僅僅能觀察對象的set方法,當(dāng)對象為集合類型時,也能觀察集合元素的增、刪、改,只不過觀察集合對象要通過KVC的mutableArrayValueForKey:方法來獲取集合。類似通過person.arr的方式獲取數(shù)組,操作數(shù)組元素的增、刪、改是觀察不到的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容