KVO

2020-05-12  本文已影响0人  mtry

写在前面

基础使用

监听一个对象的属性变化,比如监听TCKVOObjectname属性


@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation TCKVOObject

@end

// 监听
TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self 
      forKeyPath:@"name" 
         options:NSKeyValueObservingOptionNew 
         context:nil];

回调

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

移除监听

[obj removeObserver:self forKeyPath:@"name"];

触发

obj.name = @"aa";
obj.name = @"bb";

打印

//obj.name = @"aa";
name
{
    kind = 1;
    new = aa;
}
//obj.name = @"bb";
name
{
    kind = 1;
    new = bb;
}

关于 NSKeyValueObservingOptions

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) 
{
    ///改变之后的值
    NSKeyValueObservingOptionNew = 0x01,
    ///改变之前的值
    NSKeyValueObservingOptionOld = 0x02,
    ///初始值,addObserver时就会调用
    NSKeyValueObservingOptionInitial = 0x04,
    ///修改前后会调用
    NSKeyValueObservingOptionPrior = 0x08
};

监听属性的属性

比如:监听TCKVOObject中的subObj.count的变化

@interface TCKVOSubObject : NSObject

@property (nonatomic, assign) NSInteger count;

@end

@implementation TCKVOSubObject

@end

@interface TCKVOObject : NSObject

@property (nonatomic, strong) TCKVOSubObject *subObj;

@end

监听

TCKVOObject *obj = [[TCKVOObject alloc] init];
obj.subObj = [[TCKVOSubObject alloc] init];
[obj addObserver:self 
      forKeyPath:@"subObj.count" 
         options:NSKeyValueObservingOptionNew
         context:nil];

监听集合属性

比如:监听array的变化

@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSMutableArray *array;

@end

@implementation TCKVOObject

@end

监听

TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self forKeyPath:@"array"
         options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
         context:nil];

触发核心方法

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

触发

obj.array = [NSMutableArray array];
NSMutableArray *array = [obj mutableArrayValueForKey:@"array"];
[array addObject:@(1)];
[array addObject:@(2)];
[array removeObjectAtIndex:0];
array[0] = @(3);
obj.array = nil;

打印

//obj.array = [NSMutableArray array];
array
{
    kind = 1;
    new =     (
    );
    old = "<null>";
}
//[array addObject:@(1)];
array
{
    indexes = "<_NSCachedIndexSet: 0x600002368680>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        1
    );
}
//[array addObject:@(2)];
array
{
    indexes = "<_NSCachedIndexSet: 0x6000023686a0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        2
    );
}
//[array removeObjectAtIndex:0];
array
{
    indexes = "<_NSCachedIndexSet: 0x600002368680>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 3;
    old =     (
        1
    );
}
//array[0] = @(3);
array
{
    indexes = "<_NSCachedIndexSet: 0x600002368680>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 4;
    new =     (
        3
    );
    old =     (
        2
    );
}
//obj.array = nil;
array
{
    kind = 1;
    new = "<null>";
    old =     (
        3
    );
}

注意:addObject:只有newremoveObjectAtIndex:只有old,不要以集合整体来看就可以了,单看具体操作的元素就可以了,比如添加一个元素时,初始时没有的,自然是没有old值。

关于kind

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    ///设置一个新值
    NSKeyValueChangeSetting = 1,
    ///表示一个对象被插入到一对多关系的属性
    NSKeyValueChangeInsertion = 2,
    ///表示一个对象被从一对多关系的属性中移除
    NSKeyValueChangeRemoval = 3,
    ///表示一个对象在一对多的关系的属性中被替换
    NSKeyValueChangeReplacement = 4,
};

监听依赖属性

当一个属性有多个属性组成时,其中一个属性变化时,组合属性也需要变化

比如:我们需要监听string

@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSString *subString1;
@property (nonatomic, strong) NSString *subString2;
@property (nonatomic, strong) NSString *string;

@end

@implementation TCKVOObject

- (NSString *)string
{
    return [NSString stringWithFormat:@"%@-%@", self.subString1, self.subString2];
}

@end

我们需要在TCKVOObject类中添加下面方法

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key

修改后

@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSString *subString1;
@property (nonatomic, strong) NSString *subString2;
@property (nonatomic, strong) NSString *string;

@end

@implementation TCKVOObject

- (NSString *)string
{
    return [NSString stringWithFormat:@"%@-%@", self.subString1, self.subString2];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"string"])
    {
        return [NSSet setWithObjects:@"subString1", @"subString2", nil];
    }
    return [super keyPathsForValuesAffectingValueForKey:key];
}

@end

监听

TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self forKeyPath:@"string"
         options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
         context:nil];

触发

obj.subString1 = @"subString1";
obj.subString2 = @"subString2";

打印

//obj.subString1 = @"subString1";
string
{
    kind = 1;
    new = "subString1-(null)";
    old = "(null)-(null)";
}
//obj.subString2 = @"subString2";
string
{
    kind = 1;
    new = "subString1-subString2";
    old = "subString1-(null)";
}

手动触发

有时候不需要属性一改变就回调,需要在属性变化为某种条件才触发

比如:手动监听string属性

@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSString *string;

@end

@implementation TCKVOObject

@end

我们需要TCKVOObject中添加下面核心方法

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

修改后

@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSString *string;

@end

@implementation TCKVOObject

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"string"])
    {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

@end

监听

TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self forKeyPath:@"string"
         options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
         context:nil];

触发

[obj willChangeValueForKey:@"string"];
obj.string = @"aa";
[obj didChangeValueForKey:@"string"];

手动触发集合属性

监听数组array的变化

@interface TCKVOObject : NSObject

@property (nonatomic, strong) NSMutableArray *array;

@end

@implementation TCKVOObject

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"array"])
    {
        return NO;
    }
    return YES;
}

@end

触发

NSMutableArray *tmp = [obj mutableArrayValueForKey:@"array"];

[obj willChangeValueForKey:@"array"];
[tmp addObject:@"1"];
[tmp addObject:@"2"];
[obj didChangeValueForKey:@"array"];

[obj willChangeValueForKey:@"array"];
tmp[0] = @"3";
[obj didChangeValueForKey:@"array"];

打印

/*
[obj willChangeValueForKey:@"array"];
[tmp addObject:@"1"];
[tmp addObject:@"2"];
[obj didChangeValueForKey:@"array"];
*/
array
{
    kind = 1;
    new =     (
        1,
        2
    );
    old =     (
    );
}
/*
[obj willChangeValueForKey:@"array"];
tmp[0] = @"3";
[obj didChangeValueForKey:@"array"];
*/
array
{
    kind = 1;
    new =     (
        3,
        2
    );
    old =     (
        1,
        2
    );
}

小结

  1. 移除观察者之前没有添加,会闪退
  2. 只添加了观察者没有释放,当观察者释放了,触发回调会闪退(监听和移除最好成双成对)
  3. 重复添加,会多次回调

实现原理

首次为对象添加观察者时addObserver:forKeyPath:options:context,通过运行时为动态生成一个子类,把当前对象的isa指针指向新的子类,在新的子类中重写了需要观察的属性的set方法、class方法,在set方法中通过调用willChangeValueForKey:didChangeValueForKey:方法,其中在didChangeValueForKey:中调用了observeValueForKeyPath:ofObject:change:context方法进行回调,重写的class方法仍然返回的是父类,这个外部使用就没有感知了。

验证如下:

@interface TCKVOObject : NSObject

@property (nonatomic, assign) NSInteger count;

@end

@implementation TCKVOObject

- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"begin->willChangeValueForKey:%@", key);
    [super willChangeValueForKey:key];
    NSLog(@"end->willChangeValueForKey:%@", key);
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"begin->didChangeValueForKey:%@", key);
    [super didChangeValueForKey:key];
    NSLog(@"end->didChangeValueForKey:%@", key);
}

@end

//测试
{
    _obj = [[TCKVOObject alloc] init];
    [_obj addObserver:self
           forKeyPath:@"count"
              options:NSKeyValueObservingOptionNew
              context:nil];
    _obj.count ++;
}

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

/*
打印如下:
begin->willChangeValueForKey:count
end->willChangeValueForKey:count

begin->didChangeValueForKey:count
keyPath:count change:{
    kind = 1;
    new = 1;
}
end->didChangeValueForKey:count
*/

这里我们可以验证 didChangeValueForKey: 方法里调用了 observeValueForKeyPath:ofObject:change:context

添加 addObserver:forKeyPath:options:context 之前

image.png

添加 addObserver:forKeyPath:options:context 之后

image-1.png

_obj 对象的 isa 指针指向的对象为 NSKVONotifying_TCKVOObject

我们再打印出 NSKVONotifying_TCKVOObject 的全部新加方法

- (void)allMethodWithObj:(id)obj
{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(object_getClass(obj), &count);
    for(int i = 0; i < count; i++)
    {
        SEL sel = method_getName(methodList[i]);
        NSString *methodName = NSStringFromSelector(sel);
        NSLog(@"%@", methodName);
    }
}

/*
打印如下:
setCount:
class
dealloc
_isKVOA
*/

最佳实践 FBKVOController

先看效果

{
    _obj = [[TCKVOObject alloc] init];
    [self.KVOController observe:_obj keyPath:@"count" options:NSKeyValueObservingOptionNew
                          block:^(id  _Nullable observer,
                                  id  _Nonnull object,
                                  NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"%@", change);
    }];
    _obj.count ++;
}

支持Block,也不需要关注移除时机,用法非常方便。

FBKVOController 底层原理

要实现这样的效果,我们只需要把观察者回调逻辑转移至 FBKVOController,当 observeValueForKeyPath:ofObject:change:context 方法回调时,再抛出来即可。

FBKVOController 可以监听多个对象,每个对象又可以监听多个属性。可以通过 NSMapTable 存储,其中 key 为被监听的对象,而 value 用来存储被监听对象的属性,由于可以监听多个属性,可以使用列表,考虑到重复的属性没有意义,用 Set 来去重再合适不过了。

这里有个细节需要注意,假设在 A 对象中需要监听 A 对象的属性 p1,那么在 A 对象中需要持有 FBKVOController 对象,而在向 FBKVOController 对象添加监听时,需要把 A 对象传入 FBKVOController 存入 NSMapTable 的 Key 中,这就造成了循环引用。为了解决这个问题,可以通过设置 NSMapTable 的 Key 为 NSPointerFunctionsWeakMemory 通过弱引用存储。

KVO 还是有很多多线程应用场景,所以在操作 NSMapTable 时对其加锁处理就可,所以 FBKVOController 是线程安全的。

关于释放问题,当持有 FBKVOController 对象释放时,FBKVOController 对象也自动释放,在 FBKVOControllerdealloc 方法移除相关数据即可。

最后,思考一下为什么要用单例 _FBKVOSharedController 来监听回调,而不在 FBKVOController 中直接实现?

原因是:在 KVO 中,如果观察者对象释放了,这时候触发回调就会闪退,所以采用单例 _FBKVOSharedController 来监听回调就很容易避免这个问题了。

上一篇下一篇

猜你喜欢

热点阅读