// // GTMNSObject+KeyValueObserving.m // // Copyright 2009 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // // // MAKVONotificationCenter.m // MAKVONotificationCenter // // Created by Michael Ash on 10/15/08. // // This code is based on code by Michael Ash. // See comment in header. #import "GTMNSObject+KeyValueObserving.h" #import #include #import "GTMDefines.h" #import "GTMDebugSelectorValidation.h" #import "GTMMethodCheck.h" // A singleton that works as a dispatch center for KVO // -[NSObject observeValueForKeyPath:ofObject:change:context:] and turns them // into selector dispatches. It stores a collection of // GTMKeyValueObservingHelpers, and keys them via the key generated by // -dictionaryKeyForObserver:ofObject:forKeyPath:selector. @interface GTMKeyValueObservingCenter : NSObject { @private NSMutableDictionary *observerHelpers_; } + (id)defaultCenter; - (void)addObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options; - (void)removeObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector; - (id)dictionaryKeyForObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector; @end @interface GTMKeyValueObservingHelper : NSObject { @private GTM_WEAK id observer_; SEL selector_; id userInfo_; GTM_WEAK id target_; NSString* keyPath_; } - (id)initWithObserver:(id)observer object:(id)target keyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options; - (void)deregister; @end @interface GTMKeyValueChangeNotification () - (id)initWithKeyPath:(NSString *)keyPath ofObject:(id)object userInfo:(id)userInfo change:(NSDictionary *)change; @end @implementation GTMKeyValueObservingHelper // For info how and why we use these statics: // http://lists.apple.com/archives/cocoa-dev/2006/Jul/msg01038.html static char GTMKeyValueObservingHelperContextData; static char* GTMKeyValueObservingHelperContext = >MKeyValueObservingHelperContextData; - (id)initWithObserver:(id)observer object:(id)target keyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options { if((self = [super init])) { observer_ = observer; selector_ = selector; userInfo_ = [userInfo retain]; target_ = target; keyPath_ = [keyPath retain]; [target addObserver:self forKeyPath:keyPath options:options context:GTMKeyValueObservingHelperContext]; } return self; } - (NSString *)description { return [NSString stringWithFormat: @"%@ ", [self class], observer_, keyPath_, target_, NSStringFromSelector(selector_)]; } - (void)dealloc { if (target_) { _GTMDevLog(@"Didn't deregister %@", self); [self deregister]; } [userInfo_ release]; [keyPath_ release]; [super dealloc]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if(context == GTMKeyValueObservingHelperContext) { GTMKeyValueChangeNotification *notification = [[GTMKeyValueChangeNotification alloc] initWithKeyPath:keyPath ofObject:object userInfo:userInfo_ change:change]; [observer_ performSelector:selector_ withObject:notification]; [notification release]; } else { // COV_NF_START // There's no way this should ever be called. // If it is, the call will go up to NSObject which will assert. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; // COV_NF_END } } - (void)deregister { [target_ removeObserver:self forKeyPath:keyPath_]; target_ = nil; } @end @implementation GTMKeyValueObservingCenter + (id)defaultCenter { static GTMKeyValueObservingCenter *center = nil; if(!center) { // do a bit of clever atomic setting to make this thread safe // if two threads try to set simultaneously, one will fail // and the other will set things up so that the failing thread // gets the shared center GTMKeyValueObservingCenter *newCenter = [[self alloc] init]; if(!OSAtomicCompareAndSwapPtrBarrier(NULL, newCenter, (void *)¢er)) { [newCenter release]; // COV_NF_LINE no guarantee we'll hit this line } } return center; } - (id)init { if((self = [super init])) { observerHelpers_ = [[NSMutableDictionary alloc] init]; } return self; } // COV_NF_START // Singletons don't get deallocated - (void)dealloc { [observerHelpers_ release]; [super dealloc]; } // COV_NF_END - (id)dictionaryKeyForObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector { NSString *key = nil; if (!target && !keyPath && !selector) { key = [NSString stringWithFormat:@"%p:", observer]; } else { key = [NSString stringWithFormat:@"%p:%@:%p:%p", observer, keyPath, selector, target]; } return key; } - (void)addObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options { GTMKeyValueObservingHelper *helper = [[GTMKeyValueObservingHelper alloc] initWithObserver:observer object:target keyPath:keyPath selector:selector userInfo:userInfo options:options]; id key = [self dictionaryKeyForObserver:observer ofObject:target forKeyPath:keyPath selector:selector]; @synchronized(self) { GTMKeyValueObservingHelper *oldHelper = [observerHelpers_ objectForKey:key]; if (oldHelper) { _GTMDevLog(@"%@ already observing %@ forKeyPath %@", observer, target, keyPath); [oldHelper deregister]; } [observerHelpers_ setObject:helper forKey:key]; } [helper release]; } - (void)removeObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector { id key = [self dictionaryKeyForObserver:observer ofObject:target forKeyPath:keyPath selector:selector]; NSMutableArray *allValidHelperKeys = [NSMutableArray array]; NSArray *allValidHelpers = nil; @synchronized(self) { NSString *helperKey; for (helperKey in [observerHelpers_ allKeys]) { if ([helperKey hasPrefix:key]) { [allValidHelperKeys addObject:helperKey]; } } #if DEBUG if ([allValidHelperKeys count] == 0) { _GTMDevLog(@"%@ was not observing %@ with keypath %@", observer, target, keyPath); } #endif // DEBUG allValidHelpers = [observerHelpers_ objectsForKeys:allValidHelperKeys notFoundMarker:[NSNull null]]; [observerHelpers_ removeObjectsForKeys:allValidHelperKeys]; } [allValidHelpers makeObjectsPerformSelector:@selector(deregister)]; } @end @implementation NSObject (GTMKeyValueObservingAdditions) - (void)gtm_addObserver:(id)observer forKeyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options { _GTMDevAssert(observer && [keyPath length] && selector, @"Missing observer, keyPath, or selector"); GTMKeyValueObservingCenter *center = [GTMKeyValueObservingCenter defaultCenter]; GTMAssertSelectorNilOrImplementedWithArguments( observer, selector, @encode(GTMKeyValueChangeNotification *), NULL); [center addObserver:observer ofObject:self forKeyPath:keyPath selector:selector userInfo:userInfo options:options]; } - (void)gtm_removeObserver:(id)observer forKeyPath:(NSString *)keyPath selector:(SEL)selector { _GTMDevAssert(observer && [keyPath length] && selector, @"Missing observer, keyPath, or selector"); GTMKeyValueObservingCenter *center = [GTMKeyValueObservingCenter defaultCenter]; GTMAssertSelectorNilOrImplementedWithArguments( observer, selector, @encode(GTMKeyValueChangeNotification *), NULL); [center removeObserver:observer ofObject:self forKeyPath:keyPath selector:selector]; } - (void)gtm_stopObservingAllKeyPaths { GTMKeyValueObservingCenter *center = [GTMKeyValueObservingCenter defaultCenter]; [center removeObserver:self ofObject:nil forKeyPath:nil selector:Nil]; } @end @implementation GTMKeyValueChangeNotification - (id)initWithKeyPath:(NSString *)keyPath ofObject:(id)object userInfo:(id)userInfo change:(NSDictionary *)change { if ((self = [super init])) { keyPath_ = [keyPath copy]; object_ = [object retain]; userInfo_ = [userInfo retain]; change_ = [change retain]; } return self; } - (void)dealloc { [keyPath_ release]; [object_ release]; [userInfo_ release]; [change_ release]; [super dealloc]; } - (id)copyWithZone:(NSZone *)zone { return [[[self class] allocWithZone:zone] initWithKeyPath:keyPath_ ofObject:object_ userInfo:userInfo_ change:change_]; } - (BOOL)isEqual:(id)object { return ([keyPath_ isEqualToString:[object keyPath]] && [object_ isEqual:[object object]] && [userInfo_ isEqual:[object userInfo]] && [change_ isEqual:[object change]]); } - (NSString *)description { return [NSString stringWithFormat: @"%@ ", [self class], object_, keyPath_, userInfo_, change_]; } - (NSUInteger)hash { return [keyPath_ hash] + [object_ hash] + [userInfo_ hash] + [change_ hash]; } - (NSString *)keyPath { return keyPath_; } - (id)object { return object_; } - (id)userInfo { return userInfo_; } - (NSDictionary *)change { return change_; } @end #ifdef DEBUG static void SwizzleMethodsInClass(Class cls, SEL sel1, SEL sel2) { Method m1 = class_getInstanceMethod(cls, sel1); Method m2 = class_getInstanceMethod(cls, sel2); method_exchangeImplementations(m1, m2); } // This category exists to attempt to help deal with tricky KVO issues. // KVO is a wonderful technology in some ways, but is extremely fragile and // allows developers a lot of freedom to shoot themselves in the foot. // Refactoring an app that uses a lot of KVO can be really difficult, as can // debugging it. // These are some tools that we have found useful when working with KVO. Note // that these tools are only on in Debug builds. // To enable our KVO debugging, set the GTMDebugKVO environment // variable to 1 and you will get a whole pile of KVO logging that may help you // track down problems. // bash - export GTMDebugKVO=1 // csh/tcsh - setenv GTMDebugKVO 1 // @interface NSObject (GTMDebugKeyValueObserving) - (void)_gtmDebugAddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context; - (void)_gtmDebugArrayAddObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context; - (void)_gtmDebugRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; - (void)_gtmDebugArrayRemoveObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath; - (void)_gtmDebugWillChangeValueForKey:(NSString*)key; - (void)_gtmDebugDidChangeValueForKey:(NSString*)key; @end @implementation NSObject (GTMDebugKeyValueObserving) GTM_METHOD_CHECK(NSObject, _gtmDebugAddObserver:forKeyPath:options:context:); GTM_METHOD_CHECK(NSObject, _gtmDebugRemoveObserver:forKeyPath:); GTM_METHOD_CHECK(NSObject, _gtmDebugWillChangeValueForKey:); GTM_METHOD_CHECK(NSObject, _gtmDebugDidChangeValueForKey:); GTM_METHOD_CHECK(NSArray, _gtmDebugArrayAddObserver:toObjectsAtIndexes:forKeyPath:options:context:); GTM_METHOD_CHECK(NSArray, _gtmDebugArrayRemoveObserver:fromObjectsAtIndexes:forKeyPath:); + (void)load { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSDictionary *env = [[NSProcessInfo processInfo] environment]; id debugKeyValue = [env valueForKey:@"GTMDebugKVO"]; BOOL debug = NO; if ([debugKeyValue isKindOfClass:[NSNumber class]]) { debug = [debugKeyValue intValue] != 0 ? YES : NO; } else if ([debugKeyValue isKindOfClass:[NSString class]]) { debug = ([debugKeyValue hasPrefix:@"Y"] || [debugKeyValue hasPrefix:@"T"] || [debugKeyValue intValue]); } Class cls = Nil; if (debug) { cls = [NSObject class]; SwizzleMethodsInClass(cls, @selector(addObserver:forKeyPath:options:context:), @selector(_gtmDebugAddObserver:forKeyPath:options:context:)); SwizzleMethodsInClass(cls, @selector(removeObserver:forKeyPath:), @selector(_gtmDebugRemoveObserver:forKeyPath:)); SwizzleMethodsInClass(cls, @selector(willChangeValueForKey:), @selector(_gtmDebugWillChangeValueForKey:)); SwizzleMethodsInClass(cls, @selector(didChangeValueForKey:), @selector(_gtmDebugDidChangeValueForKey:)); cls = [NSArray class]; SwizzleMethodsInClass(cls, @selector(addObserver:toObjectsAtIndexes:forKeyPath:options:context:), @selector(_gtmDebugArrayAddObserver:toObjectsAtIndexes:forKeyPath:options:context:)); SwizzleMethodsInClass(cls, @selector(removeObserver:fromObjectsAtIndexes:forKeyPath:), @selector(_gtmDebugArrayRemoveObserver:fromObjectsAtIndexes:forKeyPath:)); } [pool drain]; } - (void)_gtmDebugAddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { _GTMDevLog(@"Adding observer %@ to %@ keypath '%@'", observer, self, keyPath); [self _gtmDebugAddObserver:observer forKeyPath:keyPath options:options context:context]; } - (void)_gtmDebugArrayAddObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { _GTMDevLog(@"Array adding observer %@ to indexes %@ of %@ keypath '%@'", observer, indexes, self, keyPath); [self _gtmDebugArrayAddObserver:observer toObjectsAtIndexes:indexes forKeyPath:keyPath options:options context:context]; } - (void)_gtmDebugRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { _GTMDevLog(@"Removing observer %@ from %@ keypath '%@'", observer, self, keyPath); [self _gtmDebugRemoveObserver:observer forKeyPath:keyPath]; } - (void)_gtmDebugArrayRemoveObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath { _GTMDevLog(@"Array removing observer %@ from indexes %@ of %@ keypath '%@'", indexes, observer, self, keyPath); [self _gtmDebugArrayRemoveObserver:observer fromObjectsAtIndexes:indexes forKeyPath:keyPath]; } - (void)_gtmDebugWillChangeValueForKey:(NSString*)key { _GTMDevLog(@"Will change '%@' of %@", key, self); [self _gtmDebugWillChangeValueForKey:key]; } - (void)_gtmDebugDidChangeValueForKey:(NSString*)key { _GTMDevLog(@"Did change '%@' of %@", key, self); [self _gtmDebugDidChangeValueForKey:key]; } @end #endif // DEBUG