Я отвечаю на свой вопрос здесь, потому что я видел шаблон в вопросе повсюду, но не имел ссылки на хороший пример лучшего способа. Я потерял дни, если не недели, своей жизни, чтобы отлаживать проблемы, которые в конечном итоге были вызваны добавлением и удалением наблюдателей во время доставки уведомлений 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, реализация которого мы не можем видеть, мы не можем быть полностью уверены. Учитывая это, это кажется самым безопасным способом.
Какова цель блоков try-finally? Невозможно получить такую же функциональность без них? – Monolo
Блок '@ try/@ finally' в' -addKVOOneShotObserverForKeyPath: 'защищает от исключения, создаваемого в' -addObserver: ... 'или' objc_setAssociatedObject'. Если вы считаете, что 'objc_setAssociatedObject' не может выбрасывать, то, возможно, да, вы могли бы избавиться от этого. '@ Try/@ finally' в' -observeValueForKeyPath: 'намного важнее в том, что он защищает от исключений, которые были выбраны в' block (...) ', что помешало бы исключению наблюдения и вызвало бы" block " (и что-нибудь в его лексическом закрытии), чтобы просочиться, даже если исключение было захвачено вызывающим кодом. – ipmcc
Если вы возьмете стандартное мышление какао, что любое исключенное исключение равно смерти, тогда убедитесь, что они бессмысленны, но тогда так каждый @try когда-либо. FWIW, Cocoa Bindings, которые построены на вершине KVO, регулярно бросают и проглатывают исключения, поэтому вряд ли стоит игнорировать эту возможность. – ipmcc