2013-08-13 4 views
2

Я хочу добавить наблюдение KVO, которое удаляет себя после того, как оно срабатывает один раз. Я видел много людей на StackOverflow делать такие вещи, как это:Каков хороший способ сделать одноразовое наблюдение KVO?

- (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:...can be lead to non-deterministic crashes that are hard to debug. Я также хочу быть уверенным, что это наблюдение будет вызвано только один раз (или вообще нет, если уведомление никогда не отправляется). Каков хороший способ сделать это?

ответ

3

Я отвечаю на свой вопрос здесь, потому что я видел шаблон в вопросе повсюду, но не имел ссылки на хороший пример лучшего способа. Я потерял дни, если не недели, своей жизни, чтобы отлаживать проблемы, которые в конечном итоге были вызваны добавлением и удалением наблюдателей во время доставки уведомлений 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-дугой, так что мы можем быть четко о сохранить/выпуски)

#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. Логично, что это должно быть так, поскольку наблюдаемый объект будет иметь единственное «замеченное» сохранение, но поскольку мы передаем этот объект в API, реализация которого мы не можем видеть, мы не можем быть полностью уверены. Учитывая это, это кажется самым безопасным способом.

+0

Какова цель блоков try-finally? Невозможно получить такую ​​же функциональность без них? – Monolo

+0

Блок '@ try/@ finally' в' -addKVOOneShotObserverForKeyPath: 'защищает от исключения, создаваемого в' -addObserver: ... 'или' objc_setAssociatedObject'. Если вы считаете, что 'objc_setAssociatedObject' не может выбрасывать, то, возможно, да, вы могли бы избавиться от этого. '@ Try/@ finally' в' -observeValueForKeyPath: 'намного важнее в том, что он защищает от исключений, которые были выбраны в' block (...) ', что помешало бы исключению наблюдения и вызвало бы" block " (и что-нибудь в его лексическом закрытии), чтобы просочиться, даже если исключение было захвачено вызывающим кодом. – ipmcc

+0

Если вы возьмете стандартное мышление какао, что любое исключенное исключение равно смерти, тогда убедитесь, что они бессмысленны, но тогда так каждый @try когда-либо. FWIW, Cocoa Bindings, которые построены на вершине KVO, регулярно бросают и проглатывают исключения, поэтому вряд ли стоит игнорировать эту возможность. – ipmcc

Смежные вопросы