KVO

作者: mtry | 来源:发表于2020-05-12 19:54 被阅读0次

    写在前面

    • 基础使用
    • 基本原理
    • 最佳实践 FBKVOController

    基础使用

    监听一个对象的属性变化,比如监听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 来监听回调就很容易避免这个问题了。

    相关文章

      网友评论

          本文标题:KVO

          本文链接:https://www.haomeiwen.com/subject/mfpinhtx.html