一次进行一次KVO观测的好方法是什么? [英] What's a good way to make a one-shot KVO observation?

查看:107
本文介绍了一次进行一次KVO观测的好方法是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想添加一个KVO观察值,它会在触发一次后自动删除.我在StackOverflow上看到很多人都在做这样的事情:

I want to add a KVO observation that removes itself after it fires once. I have seen lots of folks on StackOverflow doing stuff like this:

- (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];
    }
}

这似乎是合理的,但是我知道从-observeValueForKeyPath:...内部调用-removeObserver:forKeyPath:

This seems plausible, but I'm aware that calling -removeObserver:forKeyPath: from within -observeValueForKeyPath:... can be lead to non-deterministic crashes that are hard to debug. I also want to be sure this observation only gets called once (or not at all if the notification is never sent). What's a good way to do this?

推荐答案

我在这里回答自己的问题,因为我已经看到问题的模式,但是还没有提到好的方法.一个更好的方法的例子.我已经花了几天甚至几天的时间来调试问题,最终发现这些问题最终是由于在传递KVO通知期间添加和删除观察者引起的.在没有保修的情况下,我将提供以下KVO通知的实现,该通知应避免从-observeValueForKeyPath:...内部调用-addObserver:...-removeObserver:...引起的问题.代码:

I'm answering my own question here because I've seen the pattern in the question all over the place, but haven't had a reference to a good example of a better way. I've lost days, if not weeks, of my life to debugging problems ultimately found to be caused by adding and removing observers during the delivery of KVO notifications. Without warranty, I present the following implementation of a one-shot KVO notification that should avoid the problems that come from calling -addObserver:... and -removeObserver:... from inside -observeValueForKeyPath:.... The code:

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进行编译,因此我们可以明确保留/释放)

NSObject+KVOOneShot.m: (Compile with -fno-objc-arc so we can be explicit about retain/releases)

#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中的观察值,但是我的理解是,我们没有强有力的保证,当调用KVOOneShotObserver-dealloc时,观察到的对象仍将保持活动状态.从逻辑上讲,应该是这样,因为观察到的对象将具有唯一的可见"保留,但是由于我们将此对象传递给我们无法看到其实现的API,因此我们不能完全确定.鉴于此,这似乎是最安全的方法.

The only potential hitch here is that the dispatch_async deferred removal may marginally extend the lifetime of the observed object by one pass of the main run loop. This shouldn't be a big deal in the common case, but it's worth mentioning. My initial thought was to remove the observation in dealloc, but my understanding is that we don't have a strong guarantee that the observed object will still be alive when the -dealloc of KVOOneShotObserver is called. Logically, that should be the case, since the observed object will have the only "seen" retain, but since we pass this object into API whose implementation we can't see, we can't be completely sure. Given that, this feels like the safest way.

这篇关于一次进行一次KVO观测的好方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆