1

我想添加一个 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:从内部调用-observeValueForKeyPath:... 可能会导致难以调试的非确定性崩溃。我还想确保这个观察只被调用一次(或者如果从未发送过通知,则根本不调用)。有什么好方法可以做到这一点?

4

1 回答 1

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延迟删除可能会通过主运行循环的一次通过来略微延长观察对象的生命周期。这在普通情况下应该没什么大不了的,但值得一提。我最初的想法是删除 中的观察,但我的理解是,当调用ofdealloc时,我们并不能保证观察到的对象仍然存在。从逻辑上讲,应该是这样,因为观察到的对象将具有唯一的“可见”保留,但是由于我们将此对象传递给我们看不到其实现的 API,因此我们不能完全确定。鉴于此,这感觉是最安全的方式。-deallocKVOOneShotObserver

于 2013-08-13T13:29:12.280 回答