-
Star
(132)
You must be signed in to star a gist -
Fork
(33)
You must be signed in to fork a gist
-
-
Save andymatuschak/153676 to your computer and use it in GitHub Desktop.
| // | |
| // NSObject+BlockObservation.h | |
| // Version 1.0 | |
| // | |
| // Andy Matuschak | |
| // [email protected] | |
| // 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 | |
| // [email protected] | |
| // 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.