进行一次性KVO观察的好方法是什么?

时间:2013-08-13 13:29:12

标签: key-value-observing

我想添加一个KVO观察,在它发射一次之后自我移除。我在StackOverflow上看到很多人做这样的事情:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"myKeyPath"])
    {
        NSLog(@"Do stuff...");
        [object removeObserver:self forKeyPath:@"isFinished"];
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

这似乎有道理,但我知道在-removeObserver:forKeyPath: can be lead to non-deterministic crashes that are hard to debug内拨打-observeValueForKeyPath:...。我还想确保这个观察只被调用一次(如果从未发送通知则根本不调用)。有什么好办法呢?

1 个答案:

答案 0 :(得分:2)

我在这里回答我自己的问题,因为我已经在整个地方看到了问题中的模式,但没有提到一个更好的方法的好例子。我已经失去了生命中的几天,甚至几周,以调试最终发现由于在提供KVO通知期间添加和删除观察者而导致的问题。在没有保修的情况下,我提出了一次性KVO通知的以下实现,该通知应该避免来自-addObserver:...内部调用-removeObserver:...-observeValueForKeyPath:...的问题。代码:

<强> NSObject的+ KVOOneShot.h:

typedef void (^KVOOneShotObserverBlock)(NSString* keyPath, id object, NSDictionary* change, void* context);

@interface NSObject (KVOOneShot)

- (void)addKVOOneShotObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block: (KVOOneShotObserverBlock)block;

@end

NSObject + KVOOneShot.m:(使用-fno-objc-arc编译,以便我们可以明确保留/释放)

#import "NSObject+KVOOneShot.h"
#import <libkern/OSAtomic.h>
#import <objc/runtime.h>

@interface KVOOneShotObserver : NSObject
- (instancetype)initWithBlock: (KVOOneShotObserverBlock)block;
@end

@implementation NSObject (KVOOneShot)

- (void)addKVOOneShotObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block: (KVOOneShotObserverBlock)block
{
    if (!block || !keyPath)
        return;

    KVOOneShotObserver* observer = nil;
    @try
    {
        observer = [[KVOOneShotObserver alloc] initWithBlock: block];
        // Tie the observer's lifetime to the object it's observing...
        objc_setAssociatedObject(self, observer, observer, OBJC_ASSOCIATION_RETAIN);
        // Add the observation...
        [self addObserver: observer forKeyPath: keyPath options: options context: context];
    }
    @finally
    {
        // Make sure we release our hold on the observer, even if something goes wrong above. Probably paranoid of me.
        [observer release];
    }
}

@end

@implementation KVOOneShotObserver
{
   void * volatile _block;
}

- (instancetype)initWithBlock: (KVOOneShotObserverBlock)block
{
    if (self = [super init])
    {
        _block = [block copy];
    }
    return self;
}

- (void)dealloc
{
    [(id)_block release];
    [super dealloc];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    KVOOneShotObserverBlock block = (KVOOneShotObserverBlock)_block;

    // Get the block atomically, so it can only ever be executed once.
    if (block && OSAtomicCompareAndSwapPtrBarrier(block, NULL, &self->_block))
    {
        // Do it.
        @try
        {
            block(keyPath, object, change, context);
        }
        @finally
        {
            // Release it.
            [block release];

            // Remove the observation whenever...
            // Note: This can potentially extend the lifetime of the observer until the observation is removed.
            dispatch_async(dispatch_get_main_queue(), ^{
                [object removeObserver: self forKeyPath: keyPath context: context];
            });

            // Don't keep us alive any longer than necessary...
            objc_setAssociatedObject(object, self, nil, OBJC_ASSOCIATION_RETAIN);
        }
    }
}

@end

此处唯一的潜在障碍是dispatch_async延迟删除可能会通过主运行循环的一次通过略微延长观察对象的生命周期。这在普通情况下应该不是什么大问题,但值得一提。我最初的想法是删除dealloc中的观察,但我的理解是,当-dealloc KVOOneShotObserver为{{1}}时,我们没有强有力的保证观察到的对象仍然存活调用。逻辑上,应该是这种情况,因为观察对象将只有“看到”保留,但由于我们将此对象传递给我们无法看到的API,我们无法完全确定。鉴于此,这感觉就像最安全的方式。