KVO详解

KVO(Key-Value-Obersver)是iOS对于对象属性的一种源于观察者模式的设计。使用KVO可以使我们更方便地监听对象的属性的变化,再根据属性的变化去进行想要的操作。

KVO的使用

观察者模式的使用一般都是被观察对象将观察者对象加入监听,并在被观察行为发生改变时通知观察者对象。KVO也不例外,具体参考代码如下:

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@end
@implementation Person
@end

@interface Observer : NSObject
@end
@implementation Observer
//观察者对象接收通知的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"\nkeyPath : %@ \nchange : %@", keyPath, change);
    return;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        //添加监听
        [person addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        person.firstName = @"西瓜冰";
    }
    return 0;
}

打印输出

keyPath : firstName 
change : {
    kind = 1;
    new = 西瓜冰;
} 
context : 0x0

使用-addObserver:forKeyPath:options:context:方法添加被观察者指定属性在指定上下文环境下的监听,并在观察者对象中实现方法-observeValueForKeyPath:ofObject:change:context:来处理当指定属性在指定上下文环境下发生改变时所需的操作。

添加观察者

NSObject实现了以下方法来实现添加观察者:

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context;

参数主要为:

  • observer 观察者对象,值变化时通知的对象
  • keyPath 监听的属性,当属性为对象时,可以使用对象属性的属性(如:"hair.color")。
  • options 监听变化类型的组合,可多选的类型如下:
//属性此次更改的新值。
NSKeyValueObservingOptionNew
//属性此次更改前的旧值。
NSKeyValueObservingOptionOld
//如果设置了这个值,将会立刻向观察者对象发送一次通知。通知的change中以新值的方式发送被观察属性的当前值。
NSKeyValueObservingOptionInitial
//是否在属性改变前先通知一次观察者对象。如果没设置这个值,只会在属性发生改变后发送一次通知,设置了该值后会在属性发生改变前和改变后都通知一次。
NSKeyValueObservingOptionPrior
  • context 通知的上下文,当同一个观察者对同一类的不同对象同一属性或者不同类的对象的相同属性进行监听时,可以使用改参数进行区分。可以是任何参数,会作为-observeValueForKeyPath:ofObject:change:context:方法的context参数发送给观察者。

移除观察者

当观察者对象不再需要继续观察属性时,观察者对象或者被观察对象要销毁时需要移除观察者。有两个方法可以用于移除观察者,如下:

- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               context:(void *)context;
               
- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath;

几个参数分别与-addObserver:forKeyPath:options:context:对应。当没有传入context参数时(使用-removeObserver:forKeyPath:)会先移除后面加入的observer和对应的keyPath。如果同一个观察者对象在不同context下对同一被观察对象的同一属性监听时,应使用-removeObserver:forKeyPath:context:以免造成错误。

观察者监听方法

当被观察对象的对应属性发送改变时,观察者对象可以通过以下方法来接收修改的信息:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;

其中keyPathcontext分别与-addObserver:forKeyPath:options:context:对应。object为被观察者对象,change为一个记录属性变化信息的字典,可以使用以下key来访问对应信息:

// 属性变化的类型,是一个NSNumber对象,包含NSKeyValueChange枚举相关的值
NSString *const NSKeyValueChangeKindKey;

// 属性的新值。当NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加观察的方法设置了NSKeyValueObservingOptionNew时,我们能获取到属性的新值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionNew时,则我们能获取到一个NSArray对象,包含被插入的对象或
// 用于替换其它对象的对象。
NSString *const NSKeyValueChangeNewKey;

// 属性的旧值。当NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加观察的方法设置了NSKeyValueObservingOptionOld时,我们能获取到属性的旧值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionOld时,则我们能获取到一个NSArray对象,包含被移除的对象或
// 被替换的对象。
NSString *const NSKeyValueChangeOldKey;

// 如果NSKeyValueChangeKindKey的值是NSKeyValueChangeInsertion、NSKeyValueChangeRemoval
// 或者NSKeyValueChangeReplacement,则这个key对应的值是一个NSIndexSet对象,
// 包含了被插入、移除或替换的对象的索引
NSString *const NSKeyValueChangeIndexesKey;

// 当指定了NSKeyValueObservingOptionPrior选项时,在属性被修改的通知发送前,
// 会先发送一条通知给观察者。我们可以使用NSKeyValueChangeNotificationIsPriorKey
// 来获取到通知是否是预先发送的,如果是,获取到的值总是@(YES)
NSString *const NSKeyValueChangeNotificationIsPriorKey;

其中,NSKeyValueChangeKindKey的值取自于NSKeyValueChange,它的值是由以下枚举定义的:

enum {
    // 设置一个新值。被监听的属性可以是一个对象,也可以是一对一关系的属性或一对多关系的属性。
    NSKeyValueChangeSetting = 1,
    
    // 表示一个对象被插入到一对多关系的属性。
    NSKeyValueChangeInsertion = 2,
    
    // 表示一个对象被从一对多关系的属性中移除。
    NSKeyValueChangeRemoval = 3,
    
    // 表示一个对象在一对多的关系的属性中被替换
    NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

手动发送通知

默认情况下,当被监听的属性发送修改时便会发送通知,无需我们进行其他操作。但是在有些情况下(例如这个issue),自动发送通知可能会有问题。此外,如果我们需要在属性的setter方法里面进行其他操作,从而需要重写属性的setter方法时,自动发送通知方式也不够灵活。所以在以上两种情况下,可以使用手动发送通知的方式来实现KVO,代码如下:

@implementation Person
/* 实现+automaticallyNotifiesObserversForKey方法,关闭自动通知 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return ![key isEqualToString:@"lastName"];
}
/* 手动发送通知 */
- (void)setLastName:(NSString *)lastName {
    [self willChangeValueForKey:@"lastName"];
    _lastName = lastName;
    NSLog(@"\ndo something...");
    [self didChangeValueForKey:@"lastName"];
    NSLog(@"\ndo other things...");
}
@end

打印输出

keyPath : lastName 
change : {
    kind = 1;
    notificationIsPrior = 1;
} 
context : 0x0 
do something...
keyPath : lastName 
change : {
    kind = 1;
    new = soso;
} 
context : 0x0
do other things...

监听集合属性

当监听的属性为集合属性(NSMutableArrayNSMutableSetNSMutableOrderedSet)时,如果不添加其他处理,那么只会在属性赋值时有发送通知,而当属性发送变化(如[NSMutableArray-addObject])时无法进行监听,代码如下:
Person类添加NSMutableArray属性

@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *clothes;
@end

对NSMutableArray属性添加观察

Person *person = [[Person alloc] init];
Observer *observer = [[Observer alloc] init];
[person addObserver:observer forKeyPath:@"clothes" options:NSKeyValueObservingOptionNew context:NULL];
person.clothes = [[NSMutableArray alloc] init];
[person.clothes addObject:@"T-shirt"];

打印输出

keyPath : clothes 
change : {
    kind = 1;
    new =     (
    );
} 
context : 0x0

可以看出,只有在NSMutableArray赋值的时候有通知,而当NSMutableArray添加新元素时无法监听到。需要在添加元素时监听,需要使用-mutableArrayValueForKey方法获取的数组来添加属性,代码如下:
操作修改的下标和对应的值

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"\nkeyPath : %@ \nchange : %@ \ncontext : %p \ncurrent", keyPath, change, context);
    if ([keyPath isEqualToString:@"clothes"]) {
        NSArray *newArray = change[NSKeyValueChangeNewKey];
        NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
        __block NSInteger i = 0;
        [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"index : %ld, newValue : %@", idx, newArray[i]);
            i++;
        }];
    }
    return;
}
Person *person = [[Person alloc] init];
Observer *observer = [[Observer alloc] init];
[person addObserver:observer forKeyPath:@"clothes" options:NSKeyValueObservingOptionNew context:NULL];
person.clothes = [[NSMutableArray alloc] init];
NSMutableArray *clothes = [person mutableArrayValueForKey:@"clothes"];
[clothes insertObject:@"T-shirt" atIndex:0];

打印输出

keyPath : clothes 
change : {
    kind = 1;
    new =     (
    );
} 
context : 0x0

keyPath : clothes 
change : {
    indexes = "<_NSCachedIndexSet: 0x1020003d0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        "T-shirt"
    );
} 
context : 0x0
index : 0, newValue : T-shirt

通过这种方式,就能当元素发生改变时也能进行监听。使用-mutableArrayValueForKey返回的数组其实是一个NSKeyValueNotifyingMutableArray对象,通过这个对象对元素进行增改删操作时,才能实现对元素的监听,对象结构如下:

KVO详解_第1张图片
kvo_ NSKeyValueObservationInfo.png

除了添加元素外,还可以用以下的方法删除和修改元素:

-insertObject:inAtIndex:
-removeObjectFromAtIndex:
-insert:atIndexes:
-removeAtIndexes:
-replaceObjectInAtIndex:withObject:
-replaceAtIndexes:with:

属性依赖

当一个属性的变化依赖于其他属性变化时(例如一些计算属性),可以在被观察者类实现+keyPathsForValuesAffectingValueForKey类方法将它们关联起来,代码如下:

被观察者类实现依赖

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, readonly) NSString *fullName;
@end

@implementation Person
- (NSString *)fullName{
    return [self.firstName stringByAppendingString:[NSString stringWithFormat:@"%@",self.lastName]];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    if ([key isEqualToString:@"fullName"]) {
        return [NSSet setWithObjects:@"firstName", @"lastName", nil];
    } else {
        return [super keyPathsForValuesAffectingValueForKey:key];
    }
}
@end

添加观察

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        //添加监听
        [person addObserver:observer forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:NULL];
        
        person.firstName = @"西瓜冰";
        person.lastName = @"soso";
    }
    return 0;
}

打印输出

keyPath : fullName 
change : {
    kind = 1;
    new = "西瓜冰(null)";
} 
context : 0x0

keyPath : fullName 
change : {
    kind = 1;
    new = 西瓜冰soso;
} 
context : 0x0

监听信息

如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo属性,其声明如下:

// 返回一个指针,包含了被观察对象添加的所有观察者对象、注册的options等信息。
// 默认的实现是从一个全局以被观察对象的地址作为键值的字典中获取observationInfo信息。
// 为了改善性能,可以重写observationInfo属性,将这些不透明的数据指针存储在一个实例变量里。重写这个属性不能发消息(send messages)给存储的数据。(重写没试过..)
@property void *observationInfo

这个属性包含的信息如下图所示:
[图片上传失败...(image-da6f62-1527307183124)]
因为NSKeyValueObservationInfo类及NSKeyValueObservance类都是系统私有类,所以这些信息不能直接访问,需要使用KVC的valueForKeyPath:方法来访问这些私有属性。

注意事项

因为KVO的相关代码并没有开源,所以我们只能在平时的使用中总结一些注意事项,以免在一些错误上重蹈覆辙。以下是我总结的一些注意事项,如有不足,麻烦告知:

不要直接赋值实例变量

因为KVO主要通过重写了被观察对象属性的setter方法来实现通知观察者对象的,所以直接赋值实例变量时相关的通知方法将没被调用到。因此,即使是在类的内部,对于声明为属性的实例变量,不要直接使用实例变量去赋值。测试代码如下:
Person类添加一个changeLastName方法

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, readonly) NSString *fullName;

- (void)changeLastName:(NSString *)lastName;
@end

监听lastName,尝试调用changeLastName

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        //添加监听
        [person addObserver: observer forKeyPath:@"lastName" options:NSKeyValueObservingOptionNew context:NULL];
        person.firstName = @"西瓜冰";
        person.lastName = @"soso";
        
        [person changeLastName:@"moumou"];
    }
    return 0;
}

打印输出

keyPath : lastName 
change : {
    kind = 1;
    new = soso;
} 
context : 0x0

在调用changeLastName时,observer不会收到通知,所以只打印了person.lastName = @"soso";赋值时的一次通知。

不能调用NSObject类的观察方法

当在观察者对象的-observeValueForKeyPath:ofObject:change:context:方法中调用到NSObject的-observeValueForKeyPath:ofObject:change:context:会抛出一个NSInternalInconsistencyException异常,提醒你有监听的属性未作处理。测试代码如下:
修改Observer类的-observeValueForKeyPath:ofObject:change:context:方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"lastName"]) {
        NSLog(@"\nkeyPath : %@ \nchange : %@ \ncontext : %p", keyPath, change, context);
    }
    else {
        NSLog(@"\nkeyPath : %@ \nchange : %@ \ncontext : %p", keyPath, change, context);
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
    return;
}

设置监听

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        //添加监听
        [person addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        [person addObserver:observer forKeyPath:@"lastName" options:NSKeyValueObservingOptionNew context:NULL];
        person.firstName = @"西瓜冰";
        person.lastName = @"soso";
    }
    return 0;
}

Duang~,抛出异常:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: ': An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

因为KVO的被观察属性是有继承的,所以还是尽量别写[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];,这个跟使用其他系统方法时不太一样。

移除观察者对象应注意

在使用-removeObserver:forKeyPath:context:时要保证观察者对象必须存在,并且观察者对象必须监听了对应的keyPath,否则系统将抛出异常,测试代码如下:
移除未添加的observer以及观察者对象不对应的keyPath

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        Observer *observer2 = [[Observer alloc] init];
        //添加监听
        [person addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        @try {
            [person removeObserver:observer2 forKeyPath:@"firstName"];
        } @catch(NSException *exception) {
            NSLog(@"\nexception : %@", exception);
        }
        @try {
            [person removeObserver:observer forKeyPath:@"lastName"];
        } @catch(NSException *exception) {
            NSLog(@"\nexception : %@", exception);
        }
    }
    return 0;
}

打印输出

exception : Cannot remove an observer  for the key path "firstName" from  because it is not registered as an observer.

exception : Cannot remove an observer  for the key path "lastName" from  because it is not registered as an observer.
Program ended with exit code: 0

解决方法也如上面所示,使用@try@catch来捕获异常,保证程序不会crash。

可以重复添加监听

给相同的被观察对象的相同属性重复添加相同观察者对象时,将会在属性发送改变时多次调用观察者对象的-observeValueForKeyPath:ofObject:change:context:方法。从被观察对象的observationInfo属性中,我们可以看到KVO使用_observances对象来保存信息的,而如下打印所示,_observances是以NSArray实现的,而不是NSSet,所以我们就能理解为什么对完全一样的信息会发送两次通知。

info->_observances:
<__NSArrayI 0x10040fba0>(
 Context: 0x0, Property: 0x100405580>,
 Context: 0x0, Property: 0x100405580>
)

测试代码如下:
添加两次监听

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        //添加监听
        [person addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        [person addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        person.firstName = @"西瓜冰";
    }
    return 0;
}

输出如下,一次赋值,两次打印

keyPath : firstName 
change : {
    kind = 1;
    new = 西瓜冰;
} 
context : 0x0

keyPath : firstName 
change : {
    kind = 1;
    new = 西瓜冰;
} 
context : 0x0

这可能不是一件好事,因为我们在开发的过程中,可以会因为一些疏忽而重复添加了多次监听,从而导致一些我们不想要的结果。为了防止这种情况,我们可以使用runtime来比较优雅地解决。具体方法如下:
新建NSObject+KVOSafe分类,NSObject+KVOSafe.m代码

#import "NSObject+KVOSafe.h"
#import 

@interface NSMutableSet (KVOSafe)
- (void)safeAddObject:(NSObject *)object;
- (void)safeRemoveObject:(NSObject *)object;
- (BOOL)safeContainsObject:(NSObject *)object;
@end

@implementation NSMutableSet (KVOSafe)
- (NSLock *)lock {
    NSLock *_lock = objc_getAssociatedObject(self, @selector(lock));
    if (!_lock) {
        _lock = [[NSLock alloc] init];
        objc_setAssociatedObject(self, @selector(lock), _lock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _lock;
}

- (void)safeAddObject:(NSObject *)object {
    [self.lock lock];
    [self addObject:object];
    [self.lock unlock];
}

- (void)safeRemoveObject:(NSObject *)object {
    [self.lock lock];
    [self removeObject:object];
    [self.lock unlock];
}

- (BOOL)safeContainsObject:(NSObject *)object {
    [self.lock lock];
    BOOL containsObject = [self containsObject:object];
    [self.lock unlock];
    return containsObject;
}
@end

@implementation NSObject (KVOSafe)
+ (void)load {
    Method addMethod = class_getClassMethod([self class], @selector(addObserver:forKeyPath:options:context:));
    Method safeAddMethod = class_getClassMethod([self class], @selector(_safeAddObserver:forKeyPath:options:context:));
    Method removeMethod = class_getClassMethod([self class], @selector(removeObserver:forKeyPath:context:));
    Method safeRemoveMethod = class_getClassMethod([self class], @selector(_safeRemoveObserver:forKeyPath:context:));
    
    method_exchangeImplementations(addMethod, safeAddMethod);
    method_exchangeImplementations(removeMethod, safeRemoveMethod);
}

- (NSMutableSet *)safeObservers {
    NSMutableSet *_observers = objc_getAssociatedObject(self, @selector(safeObservers));
    if (!_observers) {
        _observers = [NSMutableSet set];
        objc_setAssociatedObject(self, @selector(safeObservers), _observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _observers;
}

- (void)_safeAddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    if (![self.safeObservers safeContainsObject:observer]) {
        [self.safeObservers safeAddObject:observer];
        //由于交换了IMP,这里实际调用-addObserver:forKeyPath:options:context:
        [self _safeAddObserver:observer forKeyPath:keyPath options:options context:context];
    }
}

- (void)_safeRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
    if ([self.safeObservers safeContainsObject:observer]) {
        [self.safeObservers safeRemoveObject:observer];
        //由于交换了IMP,这里实际调用-removeObserver:forKeyPath:context:
        [self _safeRemoveObserver:observer forKeyPath:keyPath context:context];
    }
}
@end

由于KVO是支持多线程的,NSMutableSet并不是线程安全,所以加了锁来保证线程安全。再执行一次,只打印了一次通知,输出如下:

keyPath : firstName 
change : {
    kind = 1;
    new = 西瓜冰;
} 
context : 0x0

但是上面的代码只是对相同的Observer进行了处理,并没有对keyPath和context做有效的处理。要对keyPath和context也进行处理将会比较麻烦,因此我们需要换一种方式实现。我们可以利用系统给我们提供的被观察对象的observationInfo属性中包含的信息来实现,具体代码如下:

@implementation NSObject (KVOSafe)
+ (void)load {
    Method addMethod = class_getClassMethod([self class], @selector(addObserver:forKeyPath:options:context:));
    Method safeAddMethod = class_getClassMethod([self class], @selector(_safeAddObserver:forKeyPath:options:context:));
    Method removeMethod = class_getClassMethod([self class], @selector(removeObserver:forKeyPath:context:));
    Method safeRemoveMethod = class_getClassMethod([self class], @selector(_safeRemoveObserver:forKeyPath:context:));
    
    method_exchangeImplementations(addMethod, safeAddMethod);
    method_exchangeImplementations(removeMethod, safeRemoveMethod);
}

- (BOOL)_containsObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
    id observationInfo = self.observationInfo;
    if (observationInfo) {
        NSArray *observances = [observationInfo valueForKey:@"_observances"];
        for (id observance in observances) {
            NSObject *_observer = [observance valueForKey:@"_observer"];
            NSString *_keyPath = [[observance valueForKeyPath:@"_property"] valueForKeyPath:@"_keyPath"];
            Ivar _contextIvar = class_getInstanceVariable([observance class], "_context");
            void *_context = (__bridge void *)(object_getIvar(observance, _contextIvar));
            if (_observer == observer && [_keyPath isEqualToString:keyPath] && _context == context) return YES;
        }
    }
    return NO;
}

- (void)_safeAddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    if (![self _containsObserver:observer forKeyPath:keyPath context:context]) {
        [self _safeAddObserver:observer forKeyPath:keyPath options:options context:context];
    }
}

- (void)_safeRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
    if ([self _containsObserver:observer forKeyPath:keyPath context:context]) {
        [self _safeRemoveObserver:observer forKeyPath:keyPath context:context];
    }
}
@end

添加观察不持有对象

使用-addObserver:forKeyPath:options:context:方法并不会对观察者对象进行retain操作。如果观察者对象被释放时,没有将监听移除的话,将会在监听属性发送改变的时报错。测试代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    self.observer = [[Observer alloc] init];
    [self.person addObserver:self.observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
    self.person.firstName = @"西瓜冰";
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.observer = nil;
        self.person.firstName = @"xxx";
    });
}

EXC_BAD_ACCESS 越界访问

Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

所以对于添加了监听的观察者对象,必须在-delloc方法里面移除监听,代码如下:

@interface Observer : NSObject
@property (nonatomic, weak) Person *person;
@end
@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"\nkeyPath : %@ \nchange : %@ \ncontext : %p", keyPath, change, context);
    return;
}
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"firstName"];
}
@end

但是这样做的话就要求观察者对象必须持有被观察者对象,所以可以看出,KVO比较适合对象需要监听其对象属性的变化的情况,例如MVC模式中Controller监听Model的变化,如果是监听关联性不强的另一个对象的变化,比较适合使用NSNotification通知机制实现。

关于KVO的实现

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,类名一般为NSKVONotifying_YourClass(如果该类名已经存在,那么YourClass类将无法使用KVO)。
在这个派生类中重写基类中被观察属性的setter方法,每添加一个被观察属性重写该属性的setter方法,未被观察属性则不重写。
派生类在被重写的setter方法实现真正的通知机制,就如前面手动发送通知那样,在重写的setter方法中调用-willChangeValueForKey-didChangeValueForKey方法。
同时派生类还重写了+class方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对setter的调用就会调用重写的setter,从而激活键值通知机制。此外,派生类还重写了dealloc方法来释放资源。

KVO详解_第2张图片
kvo_implement.png

测试代码如下:
Person类实现

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy) NSString *gender;
@end

@implementation Person

- (NSString *)description {
    Ivar isa = class_getInstanceVariable([self class], "isa");
    Class isaClass = object_getIvar(self, isa);
    NSMutableArray * methodArray = [NSMutableArray array];
    
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(isaClass, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [methodArray addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }
    free(methodList);
    
    NSString * str = [NSString stringWithFormat:
                      @"{\n\tclass : %s \n\tisa : %s \n\tmethods : %@\n}",
                      class_getName([self class]),
                      class_getName(isaClass),
                      [methodArray componentsJoinedByString:@", "]];
    return str;
}

@end

测试代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Observer *observer = [[Observer alloc] init];
        Person *person1 = [[Person alloc] init];
        Person *person2 = [[Person alloc] init];
        Person *person3 = [[Person alloc] init];
        Person *person4 = [[Person alloc] init];
        Person *control = [[Person alloc] init];
        
        NSLog(@"NSKVONotifying_Person : %p", objc_getClass("NSKVONotifying_Person"));
        [person1 addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        NSLog(@"NSKVONotifying_Person : %p\n", objc_getClass("NSKVONotifying_Person"));
        NSLog(@"person1 observered firstName : %@\n", person1);
        
        [person2 addObserver:observer forKeyPath:@"lastName" options:NSKeyValueObservingOptionNew context:NULL];
        NSLog(@"person2 observered lastName : %@\n", person2);
        
        [person3 addObserver:observer forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:NULL];
        [person3 addObserver:observer forKeyPath:@"lastName" options:NSKeyValueObservingOptionNew context:NULL];
        NSLog(@"person3 observered firstName&lastName : %@\n", person3);
        
        [person4 addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
        NSLog(@"person4 observered age : %@\n", person4);
        
        NSLog(@"control : %@\n", control);
    }
    return 0;
}

上面的代码主要测试了添加了KVO之后对象的class和isa,生成的对象的时机和重写setter方法的时机,以及如果监听的属性不存在是否会生成对应的setter方法。
打印输出

NSKVONotifying_Person : 0x0
NSKVONotifying_Person : 0x604000113b90

person1 observered firstName : {
    class : Person 
    isa : NSKVONotifying_Person 
    methods : setFirstName:, class, dealloc, _isKVOA
}

person2 observered lastName : {
    class : Person 
    isa : NSKVONotifying_Person 
    methods : setLastName:, setFirstName:, class, dealloc, _isKVOA
}

person3 observered firstName&lastName : {
    class : Person 
    isa : NSKVONotifying_Person 
    methods : setLastName:, setFirstName:, class, dealloc, _isKVOA
}

person4 observered age : {
    class : Person 
    isa : NSKVONotifying_Person 
    methods : setLastName:, setFirstName:, class, dealloc, _isKVOA
}

control : {
    class : Person 
    isa : Person 
    methods : gender, setGender:, .cxx_destruct, description, firstName, lastName, setFirstName:, setLastName:
}

第三方库KVOController

KVOController是FaceBook的一个开源库,有兴趣可以了解一下KVOController详解。这个库帮我们减少了KVO很多繁琐的操作,让我们可以享用KVO便利的同时,不必遭受代码杂乱之苦。可以通过block方式直接来实现监听,使用方法如下:

Observer *observer = [[Observer alloc] init];
Person *person = [[Person alloc] init];
[observer.KVOController observe:person
                        keyPath:FBKVOKeyPath(person.firstName)
                        options:NSKeyValueObservingOptionNew
                          block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary * _Nonnull change) {
    NSLog(@"change : %@", change);
}];
person.firstName = @"西瓜冰";

打印输出

change : {
    FBKVONotificationKeyPathKey = firstName;
    kind = 1;
    new = 西瓜冰;
}

Reference

http://southpeak.github.io/2015/04/23/cocoa-foundation-nskeyvalueobserving/
https://www.jianshu.com/p/6c6f3a24b1ef
https://blog.csdn.net/kesalin/article/details/8194240
http://blog.flight.dev.qunar.com/2016/11/09/once-ios-kvo-explore/

本文作者:西瓜冰soso
本文链接:https://www.jianshu.com/p/8deccb9c8398
温馨提示:
由于本文是原创文章,可能会有更新以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导。另外文章如有错误,请不吝指教,谢谢。

你可能感兴趣的:(KVO详解)