-
-
Save andymatuschak/153676 to your computer and use it in GitHub Desktop.
// | |
// NSObject+BlockObservation.h | |
// Version 1.0 | |
// | |
// Andy Matuschak | |
// andy@andymatuschak.org | |
// Public domain because I love you. Let me know how you use it. | |
// | |
#import <Cocoa/Cocoa.h> | |
typedef NSString AMBlockToken; | |
typedef void (^AMBlockTask)(id obj, NSDictionary *change); | |
@interface NSObject (AMBlockObservation) | |
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath task:(AMBlockTask)task; | |
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task; | |
- (void)removeObserverWithBlockToken:(AMBlockToken *)token; | |
@end |
// | |
// NSObject+BlockObservation.h | |
// Version 1.0 | |
// | |
// Andy Matuschak | |
// andy@andymatuschak.org | |
// Public domain because I love you. Let me know how you use it. | |
// | |
#import "NSObject+BlockObservation.h" | |
#import <dispatch/dispatch.h> | |
#import <objc/runtime.h> | |
@interface AMObserverTrampoline : NSObject | |
{ | |
__weak id observee; | |
NSString *keyPath; | |
AMBlockTask task; | |
NSOperationQueue *queue; | |
dispatch_once_t cancellationPredicate; | |
} | |
- (AMObserverTrampoline *)initObservingObject:(id)obj keyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task; | |
- (void)cancelObservation; | |
@end | |
@implementation AMObserverTrampoline | |
static NSString *AMObserverTrampolineContext = @"AMObserverTrampolineContext"; | |
- (AMObserverTrampoline *)initObservingObject:(id)obj keyPath:(NSString *)newKeyPath onQueue:(NSOperationQueue *)newQueue task:(AMBlockTask)newTask | |
{ | |
if (!(self = [super init])) return nil; | |
task = [newTask copy]; | |
keyPath = [newKeyPath copy]; | |
queue = [newQueue retain]; | |
observee = obj; | |
cancellationPredicate = 0; | |
[observee addObserver:self forKeyPath:keyPath options:0 context:AMObserverTrampolineContext]; | |
return self; | |
} | |
- (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context | |
{ | |
if (context == AMObserverTrampolineContext) | |
{ | |
if (queue) | |
[queue addOperationWithBlock:^{ task(object, change); }]; | |
else | |
task(object, change); | |
} | |
} | |
- (void)cancelObservation | |
{ | |
dispatch_once(&cancellationPredicate, ^{ | |
[observee removeObserver:self forKeyPath:keyPath]; | |
observee = nil; | |
}); | |
} | |
- (void)dealloc | |
{ | |
[self cancelObservation]; | |
[task release]; | |
[keyPath release]; | |
[queue release]; | |
[super dealloc]; | |
} | |
@end | |
static NSString *AMObserverMapKey = @"org.andymatuschak.observerMap"; | |
static dispatch_queue_t AMObserverMutationQueue = NULL; | |
static dispatch_queue_t AMObserverMutationQueueCreatingIfNecessary() | |
{ | |
static dispatch_once_t queueCreationPredicate = 0; | |
dispatch_once(&queueCreationPredicate, ^{ | |
AMObserverMutationQueue = dispatch_queue_create("org.andymatuschak.observerMutationQueue", 0); | |
}); | |
return AMObserverMutationQueue; | |
} | |
@implementation NSObject (AMBlockObservation) | |
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath task:(AMBlockTask)task | |
{ | |
return [self addObserverForKeyPath:keyPath onQueue:nil task:task]; | |
} | |
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task | |
{ | |
AMBlockToken *token = [[NSProcessInfo processInfo] globallyUniqueString]; | |
dispatch_sync(AMObserverMutationQueueCreatingIfNecessary(), ^{ | |
NSMutableDictionary *dict = objc_getAssociatedObject(self, AMObserverMapKey); | |
if (!dict) | |
{ | |
dict = [[NSMutableDictionary alloc] init]; | |
objc_setAssociatedObject(self, AMObserverMapKey, dict, OBJC_ASSOCIATION_RETAIN); | |
[dict release]; | |
} | |
AMObserverTrampoline *trampoline = [[AMObserverTrampoline alloc] initObservingObject:self keyPath:keyPath onQueue:queue task:task]; | |
[dict setObject:trampoline forKey:token]; | |
[trampoline release]; | |
}); | |
return token; | |
} | |
- (void)removeObserverWithBlockToken:(AMBlockToken *)token | |
{ | |
dispatch_sync(AMObserverMutationQueueCreatingIfNecessary(), ^{ | |
NSMutableDictionary *observationDictionary = objc_getAssociatedObject(self, AMObserverMapKey); | |
AMObserverTrampoline *trampoline = [observationDictionary objectForKey:token]; | |
if (!trampoline) | |
{ | |
NSLog(@"[NSObject(AMBlockObservation) removeObserverWithBlockToken]: Ignoring attempt to remove non-existent observer on %@ for token %@.", self, token); | |
return; | |
} | |
[trampoline cancelObservation]; | |
[observationDictionary removeObjectForKey:token]; | |
// Due to a bug in the obj-c runtime, this dictionary does not get cleaned up on release when running without GC. | |
if ([observationDictionary count] == 0) | |
objc_setAssociatedObject(self, AMObserverMapKey, nil, OBJC_ASSOCIATION_RETAIN); | |
}); | |
} | |
@end |
Andy, thanks for this! It's come in useful a few times. I've written a few extensions that add easier cleanup (relating the tokens to the observer, and then having the observer send -cleanupObservation
to itself in -dealloc
) as well as naïve bindings. After I've tested them more thoroughly and cleaned things up, I'll probably fork this gist or something.
Nice work, thanks for this. A couple comments:
AMObserverMapKey should not be an NSString, or any NSObject, it makes for hassles with ARC which doesn't like converting to/from a void*. Just make it a static int, and then use the address of that as the key.
Rather than use [[NSProcessInfo processInfo] globallyUniqueString], why not just use a static long counter that you increment (inside the critical region) and use NSNumber as your key.
Regardless, thanks for a very helpful chunk of code!
I believe @holleB's assessment of the undefined behavior surrounding dispatch_once_t usage as an ivar is correct.
Per Greg Parker: "[t]he implementation of dispatch_once() requires that the dispatch_once_t is zero, and has never been non-zero."
The dispatch_once usage should work fine: the cancellationPredicate is initialized to 0 when the object is created and exists on a per-object basis. Syncing onto the mutation queue isn't good enough because I'm trying to ensure that the methods body only executes once per instance.