├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Classes ├── GDConcurrencyCheckingManagedObject.h ├── GDConcurrencyCheckingManagedObject.m ├── GDCoreDataConcurrencyDebugging.h ├── NSEntityDescription+GDConcurrencyDebugging.h └── NSEntityDescription+GDConcurrencyDebugging.m ├── Example ├── EntityWithCustomClass.h ├── EntityWithCustomClass.m ├── Example-Prefix.pch ├── Example.xcdatamodeld │ ├── .xccurrentversion │ └── Example.xcdatamodel │ │ └── contents ├── _EntityWithCustomClass.h ├── _EntityWithCustomClass.m └── main.m ├── GDCoreDataConcurrencyDebugging.podspec ├── LICENSE ├── Project ├── GDCoreDataConcurrencyDebugging.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── GDCoreDataConcurrencyDebugging.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── GDCoreDataConcurrencyDebugging.xccheckout ├── Podfile ├── Podfile.lock └── Tests │ ├── CoreDataTestHelpers.h │ ├── CoreDataTestHelpers.m │ ├── ObjectArrayTests.m │ ├── Tests-Info.plist │ ├── Tests-Prefix.pch │ ├── Tests.m │ └── en.lproj │ └── InfoPlist.strings ├── README.md └── Rakefile /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | 19 | #CocoaPods 20 | Pods 21 | Project/Podfile.lock -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Vendor/fishhook"] 2 | path = Vendor/fishhook 3 | url = https://github.com/facebook/fishhook.git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # GDCoreDataConcurrencyDebugging CHANGELOG 2 | 3 | ## 0.0.7 4 | 5 | Rewrote the code to handle nested NSManagedObjectContext's. This can now handle the case of a NSPrivateQueueConcurrencyType context having an NSMainQueueConcurrencyType context as parent. 6 | 7 | ## 0.0.6 8 | 9 | Fixed validation of NSMainQueueConcurrencyType 10 | 11 | ## 0.0.5 12 | 13 | Fixed false positive caused by Core Data deallocating NSManagedObjects on a background queue 14 | 15 | ## 0.0.4 16 | 17 | More robust setting of the concurrency identifier of NSManagedObjectContext's by setting at at init time. 18 | 19 | ## 0.0.3 20 | 21 | Fixed concurrency validation when nested contexts are used and added a testsuite. 22 | 23 | ## 0.0.1 24 | 25 | Initial release. 26 | 27 | -------------------------------------------------------------------------------- /Classes/GDConcurrencyCheckingManagedObject.h: -------------------------------------------------------------------------------- 1 | // 2 | // GDConcurrencyCheckingManagedObject.h 3 | // Pods 4 | // 5 | // Created by Graham Dennis on 7/09/13. 6 | // 7 | // 8 | 9 | extern Class GDConcurrencyCheckingManagedObjectClassForClass(Class managedObjectClass); 10 | extern void GDCoreDataConcurrencyDebuggingSetFailureHandler(void (*failureFunction)(SEL _cmd)); 11 | 12 | extern void GDCoreDataConcurrencyDebuggingBeginTrackingAutorelease(); 13 | extern void GDCoreDataConcurrencyDebuggingEndTrackingAutorelease(); 14 | 15 | 16 | #ifdef NDEBUG 17 | #define GDCOREDATACONCURRENCYDEBUGGING_DISABLED 18 | #endif 19 | 20 | extern NSUInteger GDOperationQueueConcurrencyType; 21 | 22 | -------------------------------------------------------------------------------- /Classes/GDConcurrencyCheckingManagedObject.m: -------------------------------------------------------------------------------- 1 | // 2 | // GDConcurrencyCheckingManagedObject.m 3 | // Pods 4 | // 5 | // Created by Graham Dennis on 7/09/13. 6 | // 7 | // 8 | 9 | // The following includes modified versions of Mike Ash's MAZeroingWeakRef/MAZeroingWeakRef.m which is licensed under BSD. 10 | // The license for that file is as follows: 11 | // MAZeroingWeakRef and all code associated with it is distributed under a BSD license, as listed below. 12 | // 13 | // 14 | // Copyright (c) 2010, Michael Ash 15 | // All rights reserved. 16 | // 17 | // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 18 | // 19 | // Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 20 | // 21 | // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 22 | // 23 | // Neither the name of Michael Ash nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | 28 | #import "GDConcurrencyCheckingManagedObject.h" 29 | 30 | #import 31 | #import 32 | #import 33 | #import 34 | 35 | #import 36 | #import 37 | 38 | #import "fishhook.h" 39 | 40 | static pthread_mutex_t gMutex; 41 | static pthread_key_t gAutoreleaseTrackingStateKey; 42 | static pthread_key_t gInAutoreleaseKey; 43 | static NSMutableSet *gCustomSubclasses; 44 | static NSMutableDictionary *gCustomSubclassMap; // maps regular classes to their custom subclasses 45 | static NSMutableSet *gSwizzledEntityClasses; 46 | 47 | static void *GDInAutoreleaseState_NotInAutorelease = NULL; 48 | static void *GDInAutoreleaseState_InAutorelease = &GDInAutoreleaseState_InAutorelease; 49 | 50 | NSUInteger GDOperationQueueConcurrencyType = 51 | #ifdef GDCOREDATACONCURRENCYDEBUGGING_DISABLED 52 | NSConfinementConcurrencyType; 53 | #else 54 | 42; 55 | #endif 56 | 57 | #ifdef GD_CORE_DATA_CONCURRENCE_DEBUGGING_ENABLE_EXCEPTION 58 | static NSString *const GDInvalidConcurrentAccesOnReleaseException = @"GDInvalidConcurrentAccesOnReleaseException"; 59 | static NSString *const GDInvalidConcurrentAccesException = @"GDInvalidConcurrentAccesException"; 60 | #endif 61 | 62 | #define WhileLocked(block) do { \ 63 | pthread_mutex_lock(&gMutex); \ 64 | block \ 65 | pthread_mutex_unlock(&gMutex); \ 66 | } while(0) 67 | 68 | static Class CreateCustomSubclass(Class class); 69 | static void RegisterCustomSubclass(Class subclass, Class superclass); 70 | 71 | @interface GDAutoreleaseTracker : NSObject 72 | 73 | + (void)createTrackerForObject:(NSObject *)object callStack:(NSArray *)callStack; 74 | 75 | @property (nonatomic, copy) NSArray *autoreleaseBacktrace; 76 | @property (nonatomic, strong) NSObject *object; 77 | 78 | @end 79 | 80 | 81 | // Public interface 82 | Class GDConcurrencyCheckingManagedObjectClassForClass(Class managedObjectClass) 83 | { 84 | Class subclass = Nil; 85 | WhileLocked({ 86 | subclass = [gCustomSubclassMap objectForKey:managedObjectClass]; 87 | if (!subclass) { 88 | subclass = CreateCustomSubclass(managedObjectClass); 89 | RegisterCustomSubclass(subclass, managedObjectClass); 90 | } 91 | }); 92 | return subclass; 93 | } 94 | 95 | static void (*GDConcurrencyFailureFunction)(SEL _cmd) = NULL; 96 | 97 | void GDCoreDataConcurrencyDebuggingSetFailureHandler(void (*failureFunction)(SEL _cmd)) 98 | { 99 | GDConcurrencyFailureFunction = failureFunction; 100 | } 101 | 102 | #pragma mark - 103 | 104 | // COREDATA_CONCURRENCY_AVAILABLE is defined if the NSManagedObjectContext has the concurrencyType attribute 105 | #if (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_7) || (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_5_0) 106 | #define COREDATA_CONCURRENCY_AVAILABLE 107 | #endif 108 | 109 | static Class GetCustomSubclass(Class class) 110 | { 111 | WhileLocked({ 112 | while(class && ![gCustomSubclasses containsObject: class]) 113 | class = class_getSuperclass(class); 114 | }); 115 | return class; 116 | } 117 | 118 | static Class GetRealSuperclass(id obj) 119 | { 120 | Class class = GetCustomSubclass(object_getClass(obj)); 121 | NSCAssert1(class, @"Coudn't find GDCoreDataConcurrencyDebugging subclass in hierarchy starting from %@, should never happen", object_getClass(obj)); 122 | return class_getSuperclass(class); 123 | } 124 | 125 | static const void *ConcurrencyIdentifierKey = &ConcurrencyIdentifierKey; 126 | static const void *ConcurrencyTypeKey = &ConcurrencyTypeKey; 127 | static NSValue *ConcurrencyIdentifiersThreadDictionaryKey = nil; 128 | static NSValue *ConcurrencyValidAutoreleaseThreadDictionaryKey = nil; 129 | 130 | #define dispatch_current_queue() ({ \ 131 | _Pragma("clang diagnostic push"); \ 132 | _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\""); \ 133 | dispatch_queue_t queue = dispatch_get_current_queue(); \ 134 | _Pragma("clang diagnostic pop"); \ 135 | queue; \ 136 | }) 137 | static void BreakOnInvalidConcurrentAccessOnRelease(NSString *classStringRepresentation, NSArray *autoreleaseBacktrace, NSSet *invalidlyAccessedObjectsSet) 138 | 139 | { 140 | #ifndef GD_CORE_DATA_CONCURRENCE_DEBUGGING_DISABLE_LOG 141 | NSLog(@"If you want to break on invalid concurrent access, add a breakpoint on symbol BreakOnInvalidConcurrentAccessOnRelease"); 142 | NSLog(@"Invalid concurrent access to object of class '%@' caused by earlier autorelease. The autorelease pool was drained outside of the appropriate context for some managed objects. You need to add an @autoreleasepool{} directive to ensure this object is released within the NSManagedObject's queue.\nOriginal autorelease backtrace: %@; Invalidly accessed objects: %@" 143 | , classStringRepresentation 144 | , autoreleaseBacktrace 145 | , invalidlyAccessedObjectsSet); 146 | #endif 147 | 148 | #ifdef GD_CORE_DATA_CONCURRENCE_DEBUGGING_ENABLE_EXCEPTION 149 | [NSException raise:GDInvalidConcurrentAccesOnReleaseException 150 | format:@"Invalid concurrent access to object of class '%@' caused by earlier autorelease. The autorelease pool was drained outside of the appropriate context for some managed objects. You need to add an @autoreleasepool{} directive to ensure this object is released within the NSManagedObject's queue.\nOriginal autorelease backtrace: %@; Invalidly accessed objects: %@" 151 | , classStringRepresentation 152 | , autoreleaseBacktrace 153 | , invalidlyAccessedObjectsSet]; 154 | #endif 155 | } 156 | 157 | static void BreakOnInvalidConcurrentAccess(NSString *selectorStringRepresentation, NSArray *callStackSymbols) 158 | { 159 | #ifndef GD_CORE_DATA_CONCURRENCE_DEBUGGING_DISABLE_LOG 160 | NSLog(@"If you want to break on invalid concurrent access, add a breakpoint on symbol BreakOnInvalidConcurrentAccess"); 161 | NSLog(@"Invalid concurrent access to managed object calling '%@'; Stacktrace: %@" 162 | , selectorStringRepresentation 163 | , callStackSymbols); 164 | #endif 165 | 166 | #ifdef GD_CORE_DATA_CONCURRENCE_DEBUGGING_ENABLE_EXCEPTION 167 | [NSException raise:GDInvalidConcurrentAccesException 168 | format:@"Invalid concurrent access to managed object calling '%@'; Stacktrace: %@" 169 | , selectorStringRepresentation 170 | , callStackSymbols]; 171 | #endif 172 | } 173 | 174 | 175 | static BOOL ValidateConcurrencyForObjectWithExpectedIdentifier(id object, void *expectedConcurrencyIdentifier) 176 | { 177 | NSCParameterAssert(object); 178 | NSCParameterAssert(expectedConcurrencyIdentifier); 179 | 180 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 181 | NSManagedObjectContextConcurrencyType concurrencyType = (NSManagedObjectContextConcurrencyType)objc_getAssociatedObject(object, ConcurrencyTypeKey); 182 | if (concurrencyType == NSConfinementConcurrencyType) { 183 | #endif 184 | return pthread_self() == expectedConcurrencyIdentifier; 185 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 186 | } else if (concurrencyType == GDOperationQueueConcurrencyType) { 187 | NSOperationQueue *operationQueue = [NSOperationQueue currentQueue]; 188 | return (operationQueue == expectedConcurrencyIdentifier) && ([operationQueue maxConcurrentOperationCount] == 1); 189 | } else if (concurrencyType == NSMainQueueConcurrencyType && [NSThread isMainThread]) { 190 | return YES; 191 | } else { 192 | dispatch_queue_t current_queue = dispatch_current_queue(); 193 | if (current_queue == expectedConcurrencyIdentifier) return YES; 194 | NSArray *concurrencyIdentifiers = [[[NSThread currentThread] threadDictionary] objectForKey:ConcurrencyIdentifiersThreadDictionaryKey]; 195 | return [concurrencyIdentifiers containsObject:[NSValue valueWithPointer:expectedConcurrencyIdentifier]]; 196 | } 197 | #endif 198 | } 199 | 200 | static void *GetConcurrencyIdentifierForContext(NSManagedObjectContext *context) 201 | { 202 | return objc_getAssociatedObject(context, ConcurrencyIdentifierKey); 203 | } 204 | 205 | static void *GetConcurrencyTypeForContext(NSManagedObjectContext *context) 206 | { 207 | return objc_getAssociatedObject(context, ConcurrencyTypeKey); 208 | } 209 | 210 | static void SetConcurrencyIdentifierForContext(NSManagedObjectContext *context) 211 | { 212 | void *concurrencyIdentifier = GetConcurrencyIdentifierForContext(context); 213 | if (concurrencyIdentifier) return; 214 | 215 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 216 | NSManagedObjectContextConcurrencyType concurrencyType = (NSManagedObjectContextConcurrencyType)GetConcurrencyTypeForContext(context); 217 | if (concurrencyType == NSConfinementConcurrencyType) { 218 | #endif 219 | concurrencyIdentifier = pthread_self(); 220 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 221 | } else if (concurrencyType == GDOperationQueueConcurrencyType) { 222 | NSOperationQueue *operationQueue = [NSOperationQueue currentQueue]; 223 | NSCParameterAssert(operationQueue != nil); 224 | NSCParameterAssert([operationQueue maxConcurrentOperationCount] == 1); 225 | concurrencyIdentifier = (void *)operationQueue; 226 | } else if (concurrencyType == NSMainQueueConcurrencyType 227 | || concurrencyType == NSPrivateQueueConcurrencyType) { 228 | __block dispatch_queue_t confinementQueue = NULL; 229 | if (concurrencyType == NSMainQueueConcurrencyType) 230 | confinementQueue = dispatch_get_main_queue(); 231 | else { 232 | // Get the context queue by running a block on it 233 | // Note that nested -performBlockAndWait calls are safe. 234 | [context performBlockAndWait:^{ 235 | confinementQueue = dispatch_current_queue(); 236 | }]; 237 | } 238 | 239 | concurrencyIdentifier = confinementQueue; 240 | } else { 241 | NSCParameterAssert(NO); 242 | } 243 | #endif 244 | objc_setAssociatedObject(context, ConcurrencyIdentifierKey, concurrencyIdentifier, OBJC_ASSOCIATION_ASSIGN); 245 | } 246 | 247 | static BOOL ValidateConcurrency(id object, SEL _cmd) 248 | { 249 | void *desiredConcurrencyIdentifier = (void *)objc_getAssociatedObject(object, ConcurrencyIdentifierKey); 250 | if(nil == desiredConcurrencyIdentifier) { 251 | return YES; 252 | } 253 | BOOL concurrencyValid = ValidateConcurrencyForObjectWithExpectedIdentifier(object, desiredConcurrencyIdentifier); 254 | if (!concurrencyValid) { 255 | NSMutableSet *trackingState = pthread_getspecific(gAutoreleaseTrackingStateKey); 256 | if (trackingState != nil) { 257 | [trackingState addObject:object]; 258 | } else if (GDConcurrencyFailureFunction) { 259 | GDConcurrencyFailureFunction(_cmd); 260 | } else { 261 | BreakOnInvalidConcurrentAccess(NSStringFromSelector(_cmd), [NSThread callStackSymbols]); 262 | } 263 | } 264 | return concurrencyValid; 265 | } 266 | 267 | #pragma mark - Dynamic Subclass method implementations 268 | 269 | static void CustomSubclassRelease(id self, SEL _cmd) 270 | { 271 | ValidateConcurrency(self, _cmd); 272 | Class superclass = GetRealSuperclass(self); 273 | IMP superRelease = class_getMethodImplementation(superclass, _cmd); 274 | ((void (*)(id, SEL))superRelease)(self, _cmd); 275 | } 276 | 277 | static id CustomSubclassAutorelease(id self, SEL _cmd) 278 | { 279 | ValidateConcurrency(self, _cmd); 280 | Class superclass = GetRealSuperclass(self); 281 | IMP superAutorelease = class_getMethodImplementation(superclass, _cmd); 282 | return ((id (*)(id, SEL))superAutorelease)(self, _cmd); 283 | } 284 | 285 | static void CustomSubclassWillAccessValueForKey(id self, SEL _cmd, NSString *key) 286 | { 287 | ValidateConcurrency(self, _cmd); 288 | Class superclass = GetRealSuperclass(self); 289 | IMP superWillAccessValueForKey = class_getMethodImplementation(superclass, _cmd); 290 | ((void (*)(id, SEL, id))superWillAccessValueForKey)(self, _cmd, key); 291 | } 292 | 293 | static void CustomSubclassWillChangeValueForKey(id self, SEL _cmd, NSString *key) 294 | { 295 | ValidateConcurrency(self, _cmd); 296 | Class superclass = GetRealSuperclass(self); 297 | IMP superWillChangeValueForKey = class_getMethodImplementation(superclass, _cmd); 298 | ((void (*)(id, SEL, id))superWillChangeValueForKey)(self, _cmd, key); 299 | } 300 | 301 | static void CustomSubclassWillChangeValueForKeyWithSetMutationUsingObjects(id self, SEL _cmd, NSString *key, NSKeyValueSetMutationKind mutationkind, NSSet *inObjects) 302 | { 303 | ValidateConcurrency(self, _cmd); 304 | Class superclass = GetRealSuperclass(self); 305 | IMP superWillChangeValueForKeyWithSetMutationUsingObjects = class_getMethodImplementation(superclass, _cmd); 306 | ((void (*)(id, SEL, id, NSKeyValueSetMutationKind, id))superWillChangeValueForKeyWithSetMutationUsingObjects)(self, _cmd, key, mutationkind, inObjects); 307 | } 308 | 309 | static BOOL CustomSubclassIsKindOfClass(id self, SEL _cmd, Class class) 310 | { 311 | Class superclass = GetRealSuperclass(self); 312 | IMP superIsKindOfClass = class_getMethodImplementation(superclass, _cmd); 313 | BOOL result = ((BOOL (*)(id, SEL, Class))superIsKindOfClass)(self, _cmd, class); 314 | if (result) return result; 315 | 316 | Class customSubclassOfClass = GetCustomSubclass(class); 317 | if (customSubclassOfClass) { 318 | class = class_getSuperclass(customSubclassOfClass); 319 | return ((BOOL (*)(id, SEL, Class))superIsKindOfClass)(self, _cmd, class); 320 | } 321 | return result; 322 | } 323 | 324 | #pragma mark - Dynamic subclass creation and registration 325 | 326 | static Class CreateCustomSubclass(Class class) 327 | { 328 | NSString *newName = [NSString stringWithFormat: @"%s_GDCoreDataConcurrencyDebugging", class_getName(class)]; 329 | const char *newNameC = [newName UTF8String]; 330 | 331 | Class subclass = objc_allocateClassPair(class, newNameC, 0); 332 | 333 | Method release = class_getInstanceMethod(class, @selector(release)); 334 | Method autorelease = class_getInstanceMethod(class, @selector(autorelease)); 335 | Method willAccessValueForKey = class_getInstanceMethod(class, @selector(willAccessValueForKey:)); 336 | Method willChangeValueForKey = class_getInstanceMethod(class, @selector(willChangeValueForKey:)); 337 | Method willChangeValueForKeyWithSetMutationUsingObjects = class_getInstanceMethod(class, @selector(willChangeValueForKey:withSetMutation:usingObjects:)); 338 | Method isKindOfClass = class_getInstanceMethod(class, @selector(isKindOfClass:)); 339 | 340 | // We do not override dealloc because if a context has more than 300 objects it has references to, the objects will be deallocated on a background queue 341 | // This would normally be considered unsafe access, but as its Core Data doing this, we must assume it to be safe. 342 | // We shouldn't get miss any unsafe concurrency because in normal circumstances, -release will be called on the objects, which itself would trigger deallocation. 343 | 344 | class_addMethod(subclass, @selector(release), (IMP)CustomSubclassRelease, method_getTypeEncoding(release)); 345 | class_addMethod(subclass, @selector(autorelease), (IMP)CustomSubclassAutorelease, method_getTypeEncoding(autorelease)); 346 | class_addMethod(subclass, @selector(willAccessValueForKey:), (IMP)CustomSubclassWillAccessValueForKey, method_getTypeEncoding(willAccessValueForKey)); 347 | class_addMethod(subclass, @selector(willChangeValueForKey:), (IMP)CustomSubclassWillChangeValueForKey, method_getTypeEncoding(willChangeValueForKey)); 348 | class_addMethod(subclass, @selector(willChangeValueForKey:withSetMutation:usingObjects:), (IMP)CustomSubclassWillChangeValueForKeyWithSetMutationUsingObjects, method_getTypeEncoding(willChangeValueForKeyWithSetMutationUsingObjects)); 349 | class_addMethod(subclass, @selector(isKindOfClass:), (IMP)CustomSubclassIsKindOfClass, method_getTypeEncoding(isKindOfClass)); 350 | 351 | objc_registerClassPair(subclass); 352 | 353 | return subclass; 354 | } 355 | 356 | // Our pthread mutex must be held for this function 357 | static void RegisterCustomSubclass(Class subclass, Class superclass) 358 | { 359 | [gCustomSubclassMap setObject: subclass forKey: (id ) superclass]; 360 | [gCustomSubclasses addObject: subclass]; 361 | } 362 | 363 | 364 | @interface NSManagedObject (GDCoreDataConcurrencyChecking) 365 | 366 | + (Class)classForEntity:(NSEntityDescription *)entity; 367 | 368 | - (id)gd_initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context; 369 | 370 | - (Class)grd_class; 371 | 372 | @end 373 | 374 | @interface NSManagedObjectContext (GDCoreDataConcurrencyChecking) 375 | 376 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 377 | - (id)gd_initWithConcurrencyType:(NSManagedObjectContextConcurrencyType)type; 378 | #else 379 | - (id)gd_init; 380 | #endif 381 | 382 | @end 383 | 384 | @interface NSObject (AutoreleaseTracking) 385 | 386 | - (id)gd_autorelease; 387 | 388 | @end 389 | 390 | struct DispatchWrapperState { 391 | void *context; 392 | void (*function)(void *); 393 | NSSet *concurrencyIdentifiers; 394 | dispatch_block_t block; 395 | }; 396 | 397 | static void DispatchTargetFunctionWrapper(void *context) 398 | { 399 | struct DispatchWrapperState *state = (struct DispatchWrapperState *)context; 400 | 401 | NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; 402 | 403 | // Save the old concurrency identifier array, if there was one. 404 | id oldConcurrencyIdentifiers = [[threadDictionary objectForKey:ConcurrencyIdentifiersThreadDictionaryKey] retain]; 405 | 406 | [threadDictionary setObject:state->concurrencyIdentifiers forKey:ConcurrencyIdentifiersThreadDictionaryKey]; 407 | 408 | if (state->function) 409 | state->function(state->context); 410 | else 411 | state->block(); 412 | 413 | // Restore the old concurrency identifier array, if there was one. 414 | if (oldConcurrencyIdentifiers) 415 | [threadDictionary setObject:oldConcurrencyIdentifiers forKey:ConcurrencyIdentifiersThreadDictionaryKey]; 416 | else 417 | [threadDictionary removeObjectForKey:ConcurrencyIdentifiersThreadDictionaryKey]; 418 | 419 | [oldConcurrencyIdentifiers release]; 420 | } 421 | 422 | static void DispatchSyncWrapper(dispatch_queue_t queue, void *context, void (*function)(void *), dispatch_block_t block, void *dispatch_call) 423 | { 424 | // Create or obtain an array of valid concurrency identifiers for the callee block 425 | // This list of concurrency identifiers is basically a stack of the current set of queues that we are logically synchronously executing on, 426 | // even if we aren't executing on that thread. For example, if we dispatch_sync from a background queue to the main queue, the two queues will 427 | // presently be running on different threads, but the block on the main queue is essentially operating on the background queue too. 428 | NSSet *concurrencyIdentifiers = [[[NSThread currentThread] threadDictionary] objectForKey:ConcurrencyIdentifiersThreadDictionaryKey]; 429 | if (!concurrencyIdentifiers) { 430 | concurrencyIdentifiers = [NSSet set]; 431 | } 432 | concurrencyIdentifiers = [concurrencyIdentifiers setByAddingObject:[NSValue valueWithPointer:dispatch_current_queue()]]; 433 | 434 | [concurrencyIdentifiers retain]; 435 | 436 | struct DispatchWrapperState state = {context, function, concurrencyIdentifiers, block}; 437 | 438 | // Passing 'state' on the stack frame is OK because this is a sync function call 439 | if (function) { 440 | ((void (*)(dispatch_queue_t, void*, void (*)(void *)))dispatch_call)(queue, &state, DispatchTargetFunctionWrapper); 441 | } else { 442 | ((void (*)(dispatch_queue_t, dispatch_block_t))dispatch_call)(queue, ^{ 443 | DispatchTargetFunctionWrapper((void *)&state); 444 | }); 445 | } 446 | 447 | [concurrencyIdentifiers release]; 448 | } 449 | 450 | #define DISPATCH_WRAPPER(dispatch_function) \ 451 | static void (*original_ ## dispatch_function) (dispatch_queue_t, void *, void (*)(void *)); \ 452 | static void wrapper_ ## dispatch_function (dispatch_queue_t queue, void *context, void (*function)(void *)) \ 453 | { \ 454 | DispatchSyncWrapper(queue, context, function, nil, original_ ## dispatch_function); \ 455 | } 456 | 457 | #define DISPATCH_BLOCK_WRAPPER(dispatch_function) \ 458 | static void (*original_ ## dispatch_function) (dispatch_queue_t, dispatch_block_t); \ 459 | static void wrapper_ ## dispatch_function (dispatch_queue_t queue, dispatch_block_t block) \ 460 | { \ 461 | DispatchSyncWrapper(queue, NULL, NULL, block, original_ ## dispatch_function); \ 462 | } 463 | 464 | DISPATCH_WRAPPER(dispatch_sync_f); 465 | DISPATCH_WRAPPER(dispatch_barrier_sync_f); 466 | DISPATCH_BLOCK_WRAPPER(dispatch_sync); 467 | DISPATCH_BLOCK_WRAPPER(dispatch_barrier_sync); 468 | 469 | static void EmptyFunction() {} 470 | 471 | __attribute__ ((constructor)) 472 | static void Initialise() 473 | { 474 | pthread_key_create(&gAutoreleaseTrackingStateKey, NULL); 475 | pthread_key_create(&gInAutoreleaseKey, NULL); 476 | } 477 | 478 | static void GDCoreDataConcurrencyDebuggingInitialise() 479 | { 480 | static dispatch_once_t onceToken; 481 | dispatch_once(&onceToken, ^{ 482 | // Instrument dispatch_sync calls to keep track of the stack of synchronous queues. 483 | { 484 | ConcurrencyIdentifiersThreadDictionaryKey = [[NSValue valueWithPointer:&ConcurrencyIdentifiersThreadDictionaryKey] retain]; 485 | ConcurrencyValidAutoreleaseThreadDictionaryKey = [[NSValue valueWithPointer:&ConcurrencyValidAutoreleaseThreadDictionaryKey] retain]; 486 | 487 | Dl_info info; 488 | 489 | // We need to make sure every function that we're rebinding has been called in this module before they are rebound. 490 | // This ensures that when rebind_symbols is called, it will find the correct value for the symbol in the lookup table 491 | // for this module. This is then used to set the original_dispatch_* function pointers. 492 | { 493 | dispatch_queue_t q = dispatch_queue_create("foo", DISPATCH_QUEUE_SERIAL); 494 | dispatch_sync_f(q, NULL, EmptyFunction); 495 | dispatch_barrier_sync_f(q, NULL, EmptyFunction); 496 | dispatch_sync(q, ^{}); 497 | dispatch_barrier_sync(q, ^{}); 498 | dispatch_release(q); 499 | } 500 | 501 | // We need to get our module name so we know which module we know has the symbol resolved. 502 | dladdr(EmptyFunction, &info); 503 | 504 | struct rebinding rebindings[] = { 505 | {"dispatch_sync_f", wrapper_dispatch_sync_f, (void**)&original_dispatch_sync_f}, 506 | {"dispatch_barrier_sync_f", wrapper_dispatch_barrier_sync_f, (void**)&original_dispatch_barrier_sync_f}, 507 | {"dispatch_sync", wrapper_dispatch_sync, (void**)&original_dispatch_sync}, 508 | {"dispatch_barrier_sync", wrapper_dispatch_barrier_sync, (void**)&original_dispatch_barrier_sync} 509 | }; 510 | 511 | int retval = rebind_symbols(rebindings, sizeof(rebindings)/sizeof(struct rebinding)); 512 | NSCAssert(retval == 0, @"ERROR: Failed to rebind symbols. Concurrency debugging will not work!"); 513 | } 514 | 515 | // Locks for the custom subclasses 516 | pthread_mutexattr_t mutexattr; 517 | pthread_mutexattr_init(&mutexattr); 518 | pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE); 519 | pthread_mutex_init(&gMutex, &mutexattr); 520 | pthread_mutexattr_destroy(&mutexattr); 521 | 522 | gCustomSubclasses = [NSMutableSet new]; 523 | gCustomSubclassMap = [NSMutableDictionary new]; 524 | gSwizzledEntityClasses = [NSMutableSet new]; 525 | 526 | }); 527 | } 528 | 529 | static void AssignExpectedIdentifiersToObjectFromContext(id object, NSManagedObjectContext *context) 530 | { 531 | if (context) { 532 | // Assign expected concurrency identifier 533 | objc_setAssociatedObject(object, ConcurrencyIdentifierKey, GetConcurrencyIdentifierForContext(context), OBJC_ASSOCIATION_ASSIGN); 534 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 535 | // Assign concurrency type in case the context is released before this object is. 536 | objc_setAssociatedObject(object, ConcurrencyTypeKey, GetConcurrencyTypeForContext(context), OBJC_ASSOCIATION_ASSIGN); 537 | #endif 538 | } 539 | } 540 | 541 | #pragma clang diagnostic push 542 | #pragma clang diagnostic ignored "-Wincomplete-implementation" 543 | @implementation NSManagedObject (GDCoreDataConcurrencyChecking) 544 | #pragma clang diagnostic pop 545 | 546 | #ifndef GDCOREDATACONCURRENCYDEBUGGING_DISABLED 547 | + (void)load 548 | { 549 | // Swizzle some methods so we can set up when a MOC or managed object is created. 550 | NSError *error = nil; 551 | if (![self jr_swizzleMethod:@selector(initWithEntity:insertIntoManagedObjectContext:) withMethod:@selector(gd_initWithEntity:insertIntoManagedObjectContext:) error:&error]) { 552 | NSLog(@"Failed to swizzle with error: %@", error); 553 | } 554 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 555 | if (![NSManagedObjectContext jr_swizzleMethod:@selector(initWithConcurrencyType:) withMethod:@selector(gd_initWithConcurrencyType:) error:&error]) 556 | #else 557 | if (![NSManagedObjectContext jr_swizzleMethod:@selector(init) withMethod:@selector(gd_init) error:&error]) 558 | #endif 559 | { 560 | NSLog(@"Failed to swizzle with error: %@", error); 561 | } 562 | 563 | if (![NSObject jr_swizzleMethod:@selector(autorelease) withMethod:@selector(gd_autorelease) error:&error]) 564 | { 565 | NSLog(@"Failed to swizzle with error: %@", error); 566 | } 567 | 568 | if (![self jr_swizzleClassMethod:@selector(classForEntity:) withClassMethod:@selector(grd_classForEntity:) error:&error]) { 569 | NSLog(@"Failed to swizzle with error: %@", error); 570 | } 571 | } 572 | #endif 573 | 574 | + (Class)grd_classForEntity:(NSEntityDescription *)entity 575 | { 576 | Class entityClass = [self grd_classForEntity:entity]; 577 | 578 | WhileLocked({ 579 | if (![gSwizzledEntityClasses containsObject:entityClass]) { 580 | NSError *error = nil; 581 | if (![entityClass jr_swizzleMethod:@selector(class) withMethod:@selector(grd_class) error:&error]) { 582 | NSLog(@"Failed to swizzle entity class %@ due to error: %@", entityClass, error); 583 | } else { 584 | [gSwizzledEntityClasses addObject:entityClass]; 585 | } 586 | } 587 | }); 588 | 589 | return entityClass; 590 | } 591 | 592 | - (id)gd_initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context 593 | { 594 | self = [self gd_initWithEntity:entity insertIntoManagedObjectContext:context]; 595 | 596 | GDCoreDataConcurrencyDebuggingInitialise(); 597 | 598 | AssignExpectedIdentifiersToObjectFromContext(self, context); 599 | return self; 600 | } 601 | 602 | - (Class)grd_class 603 | { 604 | return GetRealSuperclass(self); 605 | } 606 | 607 | @end 608 | 609 | @implementation NSManagedObjectContext (GDCoreDataConcurrencyChecking) 610 | 611 | #ifdef COREDATA_CONCURRENCY_AVAILABLE 612 | - (id)gd_initWithConcurrencyType:(NSManagedObjectContextConcurrencyType)type 613 | { 614 | NSManagedObjectContextConcurrencyType underlyingConcurrencyType = type; 615 | if (type == GDOperationQueueConcurrencyType) 616 | underlyingConcurrencyType = NSConfinementConcurrencyType; 617 | 618 | self = [self gd_initWithConcurrencyType:underlyingConcurrencyType]; 619 | objc_setAssociatedObject(self, ConcurrencyTypeKey, (void *)type, OBJC_ASSOCIATION_ASSIGN); 620 | #else 621 | - (id)gd_init 622 | { 623 | self = [self gd_init]; 624 | #if 0 625 | }} 626 | #endif 627 | #endif 628 | 629 | GDCoreDataConcurrencyDebuggingInitialise(); 630 | 631 | SetConcurrencyIdentifierForContext(self); 632 | 633 | return self; 634 | } 635 | 636 | @end 637 | 638 | @implementation GDAutoreleaseTracker 639 | 640 | + (void)createTrackerForObject:(NSObject *)object callStack:(NSArray *)callStack 641 | { 642 | [[[GDAutoreleaseTracker alloc] initWithObject:object callStack:callStack] autorelease]; 643 | } 644 | 645 | - (void)dealloc 646 | { 647 | NSMutableSet *invalidlyAccessedObjectsSet = [NSMutableSet new]; 648 | pthread_setspecific(gAutoreleaseTrackingStateKey, invalidlyAccessedObjectsSet); 649 | 650 | Class cls = object_getClass(_object); 651 | 652 | self.object = nil; 653 | BOOL wasValidRelease = [invalidlyAccessedObjectsSet count] == 0; 654 | 655 | if (!wasValidRelease) { 656 | BreakOnInvalidConcurrentAccessOnRelease(NSStringFromClass(cls), self.autoreleaseBacktrace, invalidlyAccessedObjectsSet); 657 | } 658 | pthread_setspecific(gAutoreleaseTrackingStateKey, nil); 659 | self.autoreleaseBacktrace = nil; 660 | 661 | [super dealloc]; 662 | } 663 | 664 | - (id)initWithObject:(NSObject *)object callStack:(NSArray *)callStack 665 | { 666 | if ((self = [super init])) { 667 | self.object = object; 668 | self.autoreleaseBacktrace = callStack; 669 | } 670 | 671 | return self; 672 | } 673 | 674 | @end 675 | 676 | static int32_t GDTrackAutoreleaseCounter = 0; 677 | 678 | void GDCoreDataConcurrencyDebuggingBeginTrackingAutorelease() 679 | { 680 | OSAtomicIncrement32Barrier(&GDTrackAutoreleaseCounter); 681 | } 682 | 683 | void GDCoreDataConcurrencyDebuggingEndTrackingAutorelease() 684 | { 685 | OSAtomicDecrement32Barrier(&GDTrackAutoreleaseCounter); 686 | } 687 | 688 | @implementation NSObject (AutoreleaseTracking) 689 | 690 | - (id)gd_autorelease 691 | { 692 | if (GDTrackAutoreleaseCounter == 0) { return [self gd_autorelease]; } 693 | 694 | BOOL inAutorelease = pthread_getspecific(gInAutoreleaseKey) == GDInAutoreleaseState_InAutorelease; 695 | if (!inAutorelease) { 696 | pthread_setspecific(gInAutoreleaseKey, GDInAutoreleaseState_InAutorelease); 697 | [GDAutoreleaseTracker createTrackerForObject:self callStack:[NSThread callStackSymbols]]; 698 | 699 | pthread_setspecific(gInAutoreleaseKey, GDInAutoreleaseState_NotInAutorelease); 700 | return self; 701 | } else { 702 | return [self gd_autorelease]; 703 | } 704 | } 705 | 706 | @end -------------------------------------------------------------------------------- /Classes/GDCoreDataConcurrencyDebugging.h: -------------------------------------------------------------------------------- 1 | #import "GDConcurrencyCheckingManagedObject.h" 2 | -------------------------------------------------------------------------------- /Classes/NSEntityDescription+GDConcurrencyDebugging.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSEntityDescription+GDConcurrencyDebugging.h 3 | // GDCoreDataConcurrencyDebugging 4 | // 5 | // Created by Graham Dennis on 7/09/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSEntityDescription (GDConcurrencyDebugging) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Classes/NSEntityDescription+GDConcurrencyDebugging.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSEntityDescription+GDConcurrencyDebugging.m 3 | // GDCoreDataConcurrencyDebugging 4 | // 5 | // Created by Graham Dennis on 7/09/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import "NSEntityDescription+GDConcurrencyDebugging.h" 10 | #import "GDConcurrencyCheckingManagedObject.h" 11 | 12 | #import 13 | 14 | @implementation NSEntityDescription (GDConcurrencyDebugging) 15 | 16 | #ifndef GDCOREDATACONCURRENCYDEBUGGING_DISABLED 17 | + (void)load 18 | { 19 | NSError *error = nil; 20 | if (![self jr_swizzleMethod:@selector(managedObjectClassName) 21 | withMethod:@selector(gd_managedObjectClassName) 22 | error:&error]) { 23 | NSLog(@"Failed to swizzle with error: %@", error); 24 | } 25 | } 26 | #endif 27 | 28 | - (NSString *)gd_managedObjectClassName 29 | { 30 | NSString *normalClassName = [self gd_managedObjectClassName]; 31 | if ([self isAbstract]) return normalClassName; 32 | 33 | Class normalClass = NSClassFromString(normalClassName); 34 | Class subclass = GDConcurrencyCheckingManagedObjectClassForClass(normalClass); 35 | NSString *subclassName = NSStringFromClass(subclass); 36 | return subclassName ?: normalClassName; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Example/EntityWithCustomClass.h: -------------------------------------------------------------------------------- 1 | #import "_EntityWithCustomClass.h" 2 | 3 | @interface EntityWithCustomClass : _EntityWithCustomClass {} 4 | // Custom logic goes here. 5 | @end 6 | -------------------------------------------------------------------------------- /Example/EntityWithCustomClass.m: -------------------------------------------------------------------------------- 1 | #import "EntityWithCustomClass.h" 2 | 3 | 4 | @interface EntityWithCustomClass () 5 | 6 | // Private interface goes here. 7 | 8 | @end 9 | 10 | 11 | @implementation EntityWithCustomClass 12 | 13 | // Custom logic goes here. 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Example/Example-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'Example' target in the 'Example' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #import 8 | #endif 9 | -------------------------------------------------------------------------------- /Example/Example.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Example.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcdatamodeld/Example.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Example/_EntityWithCustomClass.h: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This file is machine-generated and constantly overwritten. 2 | // Make changes to EntityWithCustomClass.h instead. 3 | 4 | #import 5 | 6 | 7 | extern const struct EntityWithCustomClassAttributes { 8 | __unsafe_unretained NSString *name; 9 | } EntityWithCustomClassAttributes; 10 | 11 | extern const struct EntityWithCustomClassRelationships { 12 | } EntityWithCustomClassRelationships; 13 | 14 | extern const struct EntityWithCustomClassFetchedProperties { 15 | } EntityWithCustomClassFetchedProperties; 16 | 17 | 18 | 19 | 20 | @interface EntityWithCustomClassID : NSManagedObjectID {} 21 | @end 22 | 23 | @interface _EntityWithCustomClass : NSManagedObject {} 24 | + (id)insertInManagedObjectContext:(NSManagedObjectContext*)moc_; 25 | + (NSString*)entityName; 26 | + (NSEntityDescription*)entityInManagedObjectContext:(NSManagedObjectContext*)moc_; 27 | - (EntityWithCustomClassID*)objectID; 28 | 29 | 30 | 31 | 32 | 33 | @property (nonatomic, strong) NSString* name; 34 | 35 | 36 | 37 | //- (BOOL)validateName:(id*)value_ error:(NSError**)error_; 38 | 39 | 40 | 41 | 42 | 43 | 44 | @end 45 | 46 | @interface _EntityWithCustomClass (CoreDataGeneratedAccessors) 47 | 48 | @end 49 | 50 | @interface _EntityWithCustomClass (CoreDataGeneratedPrimitiveAccessors) 51 | 52 | 53 | - (NSString*)primitiveName; 54 | - (void)setPrimitiveName:(NSString*)value; 55 | 56 | 57 | 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Example/_EntityWithCustomClass.m: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This file is machine-generated and constantly overwritten. 2 | // Make changes to EntityWithCustomClass.m instead. 3 | 4 | #import "_EntityWithCustomClass.h" 5 | 6 | const struct EntityWithCustomClassAttributes EntityWithCustomClassAttributes = { 7 | .name = @"name", 8 | }; 9 | 10 | const struct EntityWithCustomClassRelationships EntityWithCustomClassRelationships = { 11 | }; 12 | 13 | const struct EntityWithCustomClassFetchedProperties EntityWithCustomClassFetchedProperties = { 14 | }; 15 | 16 | @implementation EntityWithCustomClassID 17 | @end 18 | 19 | @implementation _EntityWithCustomClass 20 | 21 | + (id)insertInManagedObjectContext:(NSManagedObjectContext*)moc_ { 22 | NSParameterAssert(moc_); 23 | return [NSEntityDescription insertNewObjectForEntityForName:@"EntityWithCustomClass" inManagedObjectContext:moc_]; 24 | } 25 | 26 | + (NSString*)entityName { 27 | return @"EntityWithCustomClass"; 28 | } 29 | 30 | + (NSEntityDescription*)entityInManagedObjectContext:(NSManagedObjectContext*)moc_ { 31 | NSParameterAssert(moc_); 32 | return [NSEntityDescription entityForName:@"EntityWithCustomClass" inManagedObjectContext:moc_]; 33 | } 34 | 35 | - (EntityWithCustomClassID*)objectID { 36 | return (EntityWithCustomClassID*)[super objectID]; 37 | } 38 | 39 | + (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key { 40 | NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; 41 | 42 | 43 | return keyPaths; 44 | } 45 | 46 | 47 | 48 | 49 | @dynamic name; 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /Example/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Example 4 | // 5 | // Created by Graham Dennis on 7/09/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import "EntityWithCustomClass.h" 10 | #import 11 | 12 | static NSManagedObjectModel *managedObjectModel() 13 | { 14 | static NSManagedObjectModel *model = nil; 15 | if (model != nil) { 16 | return model; 17 | } 18 | 19 | NSString *path = @"Example"; 20 | path = [path stringByDeletingPathExtension]; 21 | NSURL *modelURL = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:@"momd"]]; 22 | model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; 23 | 24 | return model; 25 | } 26 | 27 | static NSManagedObjectContext *managedObjectContext() 28 | { 29 | static NSManagedObjectContext *context = nil; 30 | if (context != nil) { 31 | return context; 32 | } 33 | 34 | @autoreleasepool { 35 | context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; 36 | 37 | NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel()]; 38 | [context setPersistentStoreCoordinator:coordinator]; 39 | 40 | NSString *STORE_TYPE = NSSQLiteStoreType; 41 | 42 | NSString *path = [[NSProcessInfo processInfo] arguments][0]; 43 | path = [path stringByDeletingPathExtension]; 44 | NSURL *url = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:@"sqlite"]]; 45 | 46 | NSError *error; 47 | NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE configuration:nil URL:url options:nil error:&error]; 48 | 49 | if (newStore == nil) { 50 | NSLog(@"Store Configuration Failure %@", ([error localizedDescription] != nil) ? [error localizedDescription] : @"Unknown Error"); 51 | } 52 | } 53 | return context; 54 | } 55 | 56 | @interface NSManagedObject (EntityAccessors) 57 | 58 | @property (nonatomic, strong) NSString *name; 59 | 60 | @end 61 | 62 | void ConcurrencyFailure(SEL _cmd) 63 | { 64 | NSLog(@"CoreData concurrency failure with selector: %@; stack: %@", NSStringFromSelector(_cmd), [NSThread callStackSymbols]); 65 | } 66 | 67 | // With optimisations turned off, the compiler will generate an autorelease method for this method 68 | NSManagedObject *IdentityFunction(NSManagedObject *object) 69 | { 70 | return object; 71 | } 72 | 73 | 74 | int main(int argc, const char * argv[]) 75 | { 76 | GDCoreDataConcurrencyDebuggingBeginTrackingAutorelease(); 77 | 78 | @autoreleasepool { 79 | GDCoreDataConcurrencyDebuggingSetFailureHandler(ConcurrencyFailure); 80 | // Create the managed object context 81 | NSManagedObjectContext *context1 = managedObjectContext(); 82 | 83 | NSManagedObjectContext *context2 = managedObjectContext(); 84 | 85 | NSEntityDescription *entity = [NSEntityDescription entityForName:@"Entity" inManagedObjectContext:context1]; 86 | 87 | __block NSManagedObject *objectInContext1 = nil; 88 | [context1 performBlockAndWait:^{ 89 | objectInContext1 = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context1]; 90 | objectInContext1.name = @"test"; 91 | [context1 save:NULL]; 92 | }]; 93 | 94 | @autoreleasepool { 95 | [context1 performBlockAndWait:^{ 96 | NSEntityDescription *childEntityDescription = [NSEntityDescription entityForName:@"ChildEntity" 97 | inManagedObjectContext:context1]; 98 | NSManagedObject *childEntity = [[NSManagedObject alloc] initWithEntity:childEntityDescription insertIntoManagedObjectContext:context1]; 99 | [objectInContext1 setValue:childEntity forKey:@"relatedEntity"]; 100 | [context1 save:NULL]; 101 | }]; 102 | } 103 | 104 | // Here's an obvious invalid access 105 | NSString *name = objectInContext1.name; 106 | NSLog(@"name: %@", name); 107 | 108 | __block NSArray *results = nil; 109 | 110 | @autoreleasepool { 111 | [context2 performBlockAndWait:^{ 112 | 113 | for (NSString *name in @[@"a", @"b", @"c"]) { 114 | EntityWithCustomClass *object = [EntityWithCustomClass insertInManagedObjectContext:context2]; 115 | object.name = name; 116 | } 117 | 118 | NSFetchRequest *fetchRequest = [NSFetchRequest new]; 119 | fetchRequest.entity = [EntityWithCustomClass entityInManagedObjectContext:context2]; 120 | fetchRequest.includesPendingChanges = YES; 121 | NSArray *tempResults = [context2 executeFetchRequest:fetchRequest error:NULL]; 122 | 123 | results = [[tempResults mutableCopy] copy]; // We can be sure that 'results' is not a magic CoreData NSArray 124 | }]; 125 | } 126 | 127 | @autoreleasepool { 128 | // This code is safe because we are just calling -objectID 129 | // But it's only safe as long as the context isn't reset (or deallocated) before the autorelease pool pops. 130 | NSMutableArray *objectIDs = [NSMutableArray new]; 131 | for (EntityWithCustomClass *object in results) { 132 | [objectIDs addObject:IdentityFunction(object).objectID]; 133 | } 134 | } 135 | 136 | @autoreleasepool { 137 | // Here's an example of unsafe code 138 | NSMutableArray *objectIDs = [NSMutableArray new]; 139 | for (EntityWithCustomClass *object in results) { 140 | [objectIDs addObject:IdentityFunction(object).objectID]; 141 | } 142 | // This code is not safe because the autoreleased object's will be cleaned up after the context is reset. 143 | // This is even worse if we interacted with the NSManagedObject's on a random dispatch queue, because they 144 | // pop their autorelease pools at unspecified times. 145 | results = nil; 146 | [context2 performBlockAndWait:^{ 147 | [context2 reset]; 148 | }]; 149 | } 150 | 151 | 152 | 153 | // Custom code here... 154 | // Save the managed object context 155 | [context1 performBlock:^{ 156 | NSError *error = nil; 157 | if (![context1 save:&error]) { 158 | NSLog(@"Error while saving %@", ([error localizedDescription] != nil) ? [error localizedDescription] : @"Unknown Error"); 159 | exit(1); 160 | } 161 | }]; 162 | 163 | { 164 | NSManagedObjectContext *mainQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; 165 | 166 | NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel()]; 167 | [mainQueueContext setPersistentStoreCoordinator:coordinator]; 168 | 169 | NSString *STORE_TYPE = NSSQLiteStoreType; 170 | 171 | NSString *path = [[NSProcessInfo processInfo] arguments][0]; 172 | path = [path stringByDeletingPathExtension]; 173 | NSURL *url = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:@"sqlite"]]; 174 | 175 | NSError *error; 176 | NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE configuration:nil URL:url options:nil error:&error]; 177 | 178 | if (newStore == nil) { 179 | NSLog(@"Store Configuration Failure %@", ([error localizedDescription] != nil) ? [error localizedDescription] : @"Unknown Error"); 180 | } 181 | 182 | NSFetchRequest *fetchRequest = [NSFetchRequest new]; 183 | fetchRequest.entity = [EntityWithCustomClass entityInManagedObjectContext:mainQueueContext]; 184 | fetchRequest.includesPendingChanges = YES; 185 | NSArray *tempResults = [mainQueueContext executeFetchRequest:fetchRequest error:NULL]; 186 | 187 | for (EntityWithCustomClass *o in tempResults) { 188 | [o willAccessValueForKey:nil]; 189 | } 190 | 191 | results = [[tempResults mutableCopy] copy]; // We can be sure that 'results' is not a magic CoreData NSArray 192 | results = nil; 193 | } 194 | 195 | { 196 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 197 | NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType]; 198 | 199 | NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel()]; 200 | [context setPersistentStoreCoordinator:coordinator]; 201 | 202 | NSString *STORE_TYPE = NSSQLiteStoreType; 203 | 204 | NSString *path = [[NSProcessInfo processInfo] arguments][0]; 205 | path = [path stringByDeletingPathExtension]; 206 | NSURL *url = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:@"sqlite"]]; 207 | 208 | NSError *error; 209 | NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE configuration:nil URL:url options:nil error:&error]; 210 | 211 | if (newStore == nil) { 212 | NSLog(@"Store Configuration Failure %@", ([error localizedDescription] != nil) ? [error localizedDescription] : @"Unknown Error"); 213 | } 214 | 215 | NSFetchRequest *fetchRequest = [NSFetchRequest new]; 216 | fetchRequest.entity = [EntityWithCustomClass entityInManagedObjectContext:context]; 217 | fetchRequest.includesPendingChanges = YES; 218 | NSArray *tempResults = [context executeFetchRequest:fetchRequest error:NULL]; 219 | 220 | for (EntityWithCustomClass *o in tempResults) { 221 | [o willAccessValueForKey:nil]; 222 | } 223 | 224 | results = [[tempResults mutableCopy] copy]; // We can be sure that 'results' is not a magic CoreData NSArray 225 | }); 226 | 227 | } 228 | 229 | 230 | [[NSRunLoop mainRunLoop] run]; 231 | } 232 | GDCoreDataConcurrencyDebuggingEndTrackingAutorelease(); 233 | return 0; 234 | } 235 | 236 | -------------------------------------------------------------------------------- /GDCoreDataConcurrencyDebugging.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "GDCoreDataConcurrencyDebugging" 3 | s.version = "0.4" 4 | s.summary = "Find out when you're accessing an NSManagedObject on the wrong queue/thread." 5 | s.description = <<-DESC 6 | 7 | GDCoreDataConcurrencyDebugging helps you find cases where NSManagedObject's are being called on the wrong thread or dispatch queue. 8 | Simply add it to your project and you will get a log message for every invalid access to an NSManagedObject. 9 | 10 | DESC 11 | s.homepage = "https://github.com/GrahamDennis/GDCoreDataConcurrencyDebugging" 12 | s.license = 'MIT' 13 | s.author = { "Graham Dennis" => "graham@grahamdennis.me" } 14 | s.source = { 15 | :git => "https://github.com/GrahamDennis/GDCoreDataConcurrencyDebugging.git", 16 | :tag => s.version.to_s, 17 | :submodules => true 18 | } 19 | 20 | s.ios.deployment_target = "3.1" 21 | s.osx.deployment_target = "10.6" 22 | s.requires_arc = false 23 | 24 | s.source_files = ['Classes', 'Vendor/fishhook/fishhook.{c,h}'] 25 | 26 | s.public_header_files = 'Classes/{GDCoreDataConcurrencyDebugging,GDConcurrencyCheckingManagedObject}.h' 27 | s.frameworks = 'CoreData' 28 | s.dependency 'JRSwizzle' 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Graham Dennis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Project/GDCoreDataConcurrencyDebugging.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | archiveVersion 6 | 1 7 | classes 8 | 9 | objectVersion 10 | 46 11 | objects 12 | 13 | 09545A77AF5E431CB18C6E85 14 | 15 | buildActionMask 16 | 2147483647 17 | files 18 | 19 | inputPaths 20 | 21 | isa 22 | PBXShellScriptBuildPhase 23 | name 24 | Copy Pods Resources 25 | outputPaths 26 | 27 | runOnlyForDeploymentPostprocessing 28 | 0 29 | shellPath 30 | /bin/sh 31 | shellScript 32 | "${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh" 33 | 34 | showEnvVarsInLog 35 | 0 36 | 37 | 0B4075391B2C451A98F0CE89 38 | 39 | fileRef 40 | 3A71E1F07DBB4D30B4C6B980 41 | isa 42 | PBXBuildFile 43 | 44 | 30F0D11EC31C87E63CB8FC25 45 | 46 | children 47 | 48 | 68996754EACCFF5C951B1AB7 49 | D0F2472B42C44AC0544C862D 50 | 51 | isa 52 | PBXGroup 53 | name 54 | Pods 55 | sourceTree 56 | <group> 57 | 58 | 3A71E1F07DBB4D30B4C6B980 59 | 60 | explicitFileType 61 | archive.ar 62 | includeInIndex 63 | 0 64 | isa 65 | PBXFileReference 66 | path 67 | libPods.a 68 | sourceTree 69 | BUILT_PRODUCTS_DIR 70 | 71 | 4C1EF852DA514B64B76DE949 72 | 73 | buildActionMask 74 | 2147483647 75 | files 76 | 77 | inputPaths 78 | 79 | isa 80 | PBXShellScriptBuildPhase 81 | name 82 | Check Pods Manifest.lock 83 | outputPaths 84 | 85 | runOnlyForDeploymentPostprocessing 86 | 0 87 | shellPath 88 | /bin/sh 89 | shellScript 90 | diff "${PODS_ROOT}/../Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null 91 | if [[ $? != 0 ]] ; then 92 | cat << EOM 93 | error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. 94 | EOM 95 | exit 1 96 | fi 97 | 98 | showEnvVarsInLog 99 | 0 100 | 101 | 68996754EACCFF5C951B1AB7 102 | 103 | includeInIndex 104 | 1 105 | isa 106 | PBXFileReference 107 | lastKnownFileType 108 | text.xcconfig 109 | name 110 | Pods.debug.xcconfig 111 | path 112 | Pods/Target Support Files/Pods/Pods.debug.xcconfig 113 | sourceTree 114 | <group> 115 | 116 | 82D63420BD2A45569D012B38 117 | 118 | buildActionMask 119 | 2147483647 120 | files 121 | 122 | inputPaths 123 | 124 | isa 125 | PBXShellScriptBuildPhase 126 | name 127 | Copy Pods Resources 128 | outputPaths 129 | 130 | runOnlyForDeploymentPostprocessing 131 | 0 132 | shellPath 133 | /bin/sh 134 | shellScript 135 | "${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh" 136 | 137 | showEnvVarsInLog 138 | 0 139 | 140 | 89502BC7CC8045B4A1E1CF78 141 | 142 | fileRef 143 | 3A71E1F07DBB4D30B4C6B980 144 | isa 145 | PBXBuildFile 146 | 147 | AC4B21589FA74F15B4823EDE 148 | 149 | buildActionMask 150 | 2147483647 151 | files 152 | 153 | inputPaths 154 | 155 | isa 156 | PBXShellScriptBuildPhase 157 | name 158 | Check Pods Manifest.lock 159 | outputPaths 160 | 161 | runOnlyForDeploymentPostprocessing 162 | 0 163 | shellPath 164 | /bin/sh 165 | shellScript 166 | diff "${PODS_ROOT}/../Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null 167 | if [[ $? != 0 ]] ; then 168 | cat << EOM 169 | error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. 170 | EOM 171 | exit 1 172 | fi 173 | 174 | showEnvVarsInLog 175 | 0 176 | 177 | BF4F45231830B0BC004A91CE 178 | 179 | buildActionMask 180 | 2147483647 181 | files 182 | 183 | BF63465D1831D99200BAE279 184 | BF63465A1831D96E00BAE279 185 | BFE657281831D70700D8C601 186 | BFE657291831D70700D8C601 187 | BFE657271831D6FF00D8C601 188 | BF4F45311830B0BC004A91CE 189 | 190 | isa 191 | PBXSourcesBuildPhase 192 | runOnlyForDeploymentPostprocessing 193 | 0 194 | 195 | BF4F45241830B0BC004A91CE 196 | 197 | buildActionMask 198 | 2147483647 199 | files 200 | 201 | BF4F45291830B0BC004A91CE 202 | 0B4075391B2C451A98F0CE89 203 | 204 | isa 205 | PBXFrameworksBuildPhase 206 | runOnlyForDeploymentPostprocessing 207 | 0 208 | 209 | BF4F45251830B0BC004A91CE 210 | 211 | buildActionMask 212 | 2147483647 213 | files 214 | 215 | BF4F452F1830B0BC004A91CE 216 | 217 | isa 218 | PBXResourcesBuildPhase 219 | runOnlyForDeploymentPostprocessing 220 | 0 221 | 222 | BF4F45261830B0BC004A91CE 223 | 224 | buildConfigurationList 225 | BF4F45371830B0BC004A91CE 226 | buildPhases 227 | 228 | AC4B21589FA74F15B4823EDE 229 | BF4F45231830B0BC004A91CE 230 | BF4F45241830B0BC004A91CE 231 | BF4F45251830B0BC004A91CE 232 | 82D63420BD2A45569D012B38 233 | 234 | buildRules 235 | 236 | dependencies 237 | 238 | isa 239 | PBXNativeTarget 240 | name 241 | Tests 242 | productName 243 | Tests 244 | productReference 245 | BF4F45271830B0BC004A91CE 246 | productType 247 | com.apple.product-type.bundle.unit-test 248 | 249 | BF4F45271830B0BC004A91CE 250 | 251 | explicitFileType 252 | wrapper.cfbundle 253 | includeInIndex 254 | 0 255 | isa 256 | PBXFileReference 257 | path 258 | Tests.xctest 259 | sourceTree 260 | BUILT_PRODUCTS_DIR 261 | 262 | BF4F45281830B0BC004A91CE 263 | 264 | isa 265 | PBXFileReference 266 | lastKnownFileType 267 | wrapper.framework 268 | name 269 | XCTest.framework 270 | path 271 | Library/Frameworks/XCTest.framework 272 | sourceTree 273 | DEVELOPER_DIR 274 | 275 | BF4F45291830B0BC004A91CE 276 | 277 | fileRef 278 | BF4F45281830B0BC004A91CE 279 | isa 280 | PBXBuildFile 281 | 282 | BF4F452A1830B0BC004A91CE 283 | 284 | children 285 | 286 | BF63465B1831D99200BAE279 287 | BF63465C1831D99200BAE279 288 | BF4F45301830B0BC004A91CE 289 | BF6346591831D96E00BAE279 290 | BF4F452B1830B0BC004A91CE 291 | 292 | isa 293 | PBXGroup 294 | path 295 | Tests 296 | sourceTree 297 | <group> 298 | 299 | BF4F452B1830B0BC004A91CE 300 | 301 | children 302 | 303 | BF4F452C1830B0BC004A91CE 304 | BF4F452D1830B0BC004A91CE 305 | BF4F45321830B0BC004A91CE 306 | 307 | isa 308 | PBXGroup 309 | name 310 | Supporting Files 311 | sourceTree 312 | <group> 313 | 314 | BF4F452C1830B0BC004A91CE 315 | 316 | isa 317 | PBXFileReference 318 | lastKnownFileType 319 | text.plist.xml 320 | path 321 | Tests-Info.plist 322 | sourceTree 323 | <group> 324 | 325 | BF4F452D1830B0BC004A91CE 326 | 327 | children 328 | 329 | BF4F452E1830B0BC004A91CE 330 | 331 | isa 332 | PBXVariantGroup 333 | name 334 | InfoPlist.strings 335 | sourceTree 336 | <group> 337 | 338 | BF4F452E1830B0BC004A91CE 339 | 340 | isa 341 | PBXFileReference 342 | lastKnownFileType 343 | text.plist.strings 344 | name 345 | en 346 | path 347 | en.lproj/InfoPlist.strings 348 | sourceTree 349 | <group> 350 | 351 | BF4F452F1830B0BC004A91CE 352 | 353 | fileRef 354 | BF4F452D1830B0BC004A91CE 355 | isa 356 | PBXBuildFile 357 | 358 | BF4F45301830B0BC004A91CE 359 | 360 | isa 361 | PBXFileReference 362 | lastKnownFileType 363 | sourcecode.c.objc 364 | path 365 | Tests.m 366 | sourceTree 367 | <group> 368 | 369 | BF4F45311830B0BC004A91CE 370 | 371 | fileRef 372 | BF4F45301830B0BC004A91CE 373 | isa 374 | PBXBuildFile 375 | 376 | BF4F45321830B0BC004A91CE 377 | 378 | isa 379 | PBXFileReference 380 | lastKnownFileType 381 | sourcecode.c.h 382 | path 383 | Tests-Prefix.pch 384 | sourceTree 385 | <group> 386 | 387 | BF4F45351830B0BC004A91CE 388 | 389 | buildSettings 390 | 391 | CLANG_ENABLE_OBJC_ARC 392 | YES 393 | CLANG_WARN_BOOL_CONVERSION 394 | YES 395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE 396 | YES_ERROR 397 | CLANG_WARN_OBJC_ROOT_CLASS 398 | YES_ERROR 399 | COMBINE_HIDPI_IMAGES 400 | YES 401 | FRAMEWORK_SEARCH_PATHS 402 | 403 | $(DEVELOPER_FRAMEWORKS_DIR) 404 | $(inherited) 405 | 406 | GCC_PRECOMPILE_PREFIX_HEADER 407 | YES 408 | GCC_PREFIX_HEADER 409 | Tests/Tests-Prefix.pch 410 | GCC_PREPROCESSOR_DEFINITIONS 411 | 412 | DEBUG=1 413 | $(inherited) 414 | 415 | GCC_WARN_ABOUT_RETURN_TYPE 416 | YES_ERROR 417 | GCC_WARN_UNDECLARED_SELECTOR 418 | YES 419 | GCC_WARN_UNUSED_FUNCTION 420 | YES 421 | INFOPLIST_FILE 422 | Tests/Tests-Info.plist 423 | PRODUCT_NAME 424 | $(TARGET_NAME) 425 | WRAPPER_EXTENSION 426 | xctest 427 | 428 | isa 429 | XCBuildConfiguration 430 | name 431 | Debug 432 | 433 | BF4F45361830B0BC004A91CE 434 | 435 | buildSettings 436 | 437 | CLANG_ENABLE_OBJC_ARC 438 | YES 439 | CLANG_WARN_BOOL_CONVERSION 440 | YES 441 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE 442 | YES_ERROR 443 | CLANG_WARN_OBJC_ROOT_CLASS 444 | YES_ERROR 445 | COMBINE_HIDPI_IMAGES 446 | YES 447 | ENABLE_NS_ASSERTIONS 448 | NO 449 | FRAMEWORK_SEARCH_PATHS 450 | 451 | $(DEVELOPER_FRAMEWORKS_DIR) 452 | $(inherited) 453 | 454 | GCC_PRECOMPILE_PREFIX_HEADER 455 | YES 456 | GCC_PREFIX_HEADER 457 | Tests/Tests-Prefix.pch 458 | GCC_WARN_ABOUT_RETURN_TYPE 459 | YES_ERROR 460 | GCC_WARN_UNDECLARED_SELECTOR 461 | YES 462 | GCC_WARN_UNUSED_FUNCTION 463 | YES 464 | INFOPLIST_FILE 465 | Tests/Tests-Info.plist 466 | PRODUCT_NAME 467 | $(TARGET_NAME) 468 | WRAPPER_EXTENSION 469 | xctest 470 | 471 | isa 472 | XCBuildConfiguration 473 | name 474 | Release 475 | 476 | BF4F45371830B0BC004A91CE 477 | 478 | buildConfigurations 479 | 480 | BF4F45351830B0BC004A91CE 481 | BF4F45361830B0BC004A91CE 482 | 483 | defaultConfigurationIsVisible 484 | 0 485 | defaultConfigurationName 486 | Release 487 | isa 488 | XCConfigurationList 489 | 490 | BF6346591831D96E00BAE279 491 | 492 | fileEncoding 493 | 4 494 | isa 495 | PBXFileReference 496 | lastKnownFileType 497 | sourcecode.c.objc 498 | path 499 | ObjectArrayTests.m 500 | sourceTree 501 | <group> 502 | 503 | BF63465A1831D96E00BAE279 504 | 505 | fileRef 506 | BF6346591831D96E00BAE279 507 | isa 508 | PBXBuildFile 509 | 510 | BF63465B1831D99200BAE279 511 | 512 | fileEncoding 513 | 4 514 | isa 515 | PBXFileReference 516 | lastKnownFileType 517 | sourcecode.c.h 518 | path 519 | CoreDataTestHelpers.h 520 | sourceTree 521 | <group> 522 | 523 | BF63465C1831D99200BAE279 524 | 525 | fileEncoding 526 | 4 527 | isa 528 | PBXFileReference 529 | lastKnownFileType 530 | sourcecode.c.objc 531 | path 532 | CoreDataTestHelpers.m 533 | sourceTree 534 | <group> 535 | 536 | BF63465D1831D99200BAE279 537 | 538 | fileRef 539 | BF63465C1831D99200BAE279 540 | isa 541 | PBXBuildFile 542 | 543 | BFD3EB7917DA869400C84390 544 | 545 | children 546 | 547 | BFD3EBA917DA884400C84390 548 | BF4F452A1830B0BC004A91CE 549 | BFD3EB8417DA869400C84390 550 | BFD3EB8317DA869400C84390 551 | 30F0D11EC31C87E63CB8FC25 552 | 553 | isa 554 | PBXGroup 555 | sourceTree 556 | <group> 557 | 558 | BFD3EB7A17DA869400C84390 559 | 560 | attributes 561 | 562 | LastUpgradeCheck 563 | 0510 564 | ORGANIZATIONNAME 565 | Graham Dennis 566 | TargetAttributes 567 | 568 | BF4F45261830B0BC004A91CE 569 | 570 | TestTargetID 571 | BFD3EBA317DA884400C84390 572 | 573 | 574 | 575 | buildConfigurationList 576 | BFD3EB7D17DA869400C84390 577 | compatibilityVersion 578 | Xcode 3.2 579 | developmentRegion 580 | English 581 | hasScannedForEncodings 582 | 0 583 | isa 584 | PBXProject 585 | knownRegions 586 | 587 | en 588 | 589 | mainGroup 590 | BFD3EB7917DA869400C84390 591 | productRefGroup 592 | BFD3EB8317DA869400C84390 593 | projectDirPath 594 | 595 | projectReferences 596 | 597 | projectRoot 598 | 599 | targets 600 | 601 | BFD3EBA317DA884400C84390 602 | BFD3EBBD17DA95F300C84390 603 | BF4F45261830B0BC004A91CE 604 | 605 | 606 | BFD3EB7D17DA869400C84390 607 | 608 | buildConfigurations 609 | 610 | BFD3EB9117DA869400C84390 611 | BFD3EB9217DA869400C84390 612 | 613 | defaultConfigurationIsVisible 614 | 0 615 | defaultConfigurationName 616 | Release 617 | isa 618 | XCConfigurationList 619 | 620 | BFD3EB8317DA869400C84390 621 | 622 | children 623 | 624 | BFD3EBA417DA884400C84390 625 | BF4F45271830B0BC004A91CE 626 | 627 | isa 628 | PBXGroup 629 | name 630 | Products 631 | sourceTree 632 | <group> 633 | 634 | BFD3EB8417DA869400C84390 635 | 636 | children 637 | 638 | BFD3EB8517DA869400C84390 639 | BFD3EBA517DA884400C84390 640 | BFD3EBA717DA884400C84390 641 | BF4F45281830B0BC004A91CE 642 | BFD3EB8717DA869400C84390 643 | 3A71E1F07DBB4D30B4C6B980 644 | 645 | isa 646 | PBXGroup 647 | name 648 | Frameworks 649 | sourceTree 650 | <group> 651 | 652 | BFD3EB8517DA869400C84390 653 | 654 | isa 655 | PBXFileReference 656 | lastKnownFileType 657 | wrapper.framework 658 | name 659 | Cocoa.framework 660 | path 661 | System/Library/Frameworks/Cocoa.framework 662 | sourceTree 663 | SDKROOT 664 | 665 | BFD3EB8717DA869400C84390 666 | 667 | children 668 | 669 | BFD3EB8817DA869400C84390 670 | BFD3EB8917DA869400C84390 671 | BFD3EB8A17DA869400C84390 672 | 673 | isa 674 | PBXGroup 675 | name 676 | Other Frameworks 677 | sourceTree 678 | <group> 679 | 680 | BFD3EB8817DA869400C84390 681 | 682 | isa 683 | PBXFileReference 684 | lastKnownFileType 685 | wrapper.framework 686 | name 687 | AppKit.framework 688 | path 689 | System/Library/Frameworks/AppKit.framework 690 | sourceTree 691 | SDKROOT 692 | 693 | BFD3EB8917DA869400C84390 694 | 695 | isa 696 | PBXFileReference 697 | lastKnownFileType 698 | wrapper.framework 699 | name 700 | CoreData.framework 701 | path 702 | System/Library/Frameworks/CoreData.framework 703 | sourceTree 704 | SDKROOT 705 | 706 | BFD3EB8A17DA869400C84390 707 | 708 | isa 709 | PBXFileReference 710 | lastKnownFileType 711 | wrapper.framework 712 | name 713 | Foundation.framework 714 | path 715 | System/Library/Frameworks/Foundation.framework 716 | sourceTree 717 | SDKROOT 718 | 719 | BFD3EB9117DA869400C84390 720 | 721 | buildSettings 722 | 723 | ALWAYS_SEARCH_USER_PATHS 724 | NO 725 | CLANG_CXX_LANGUAGE_STANDARD 726 | gnu++0x 727 | CLANG_CXX_LIBRARY 728 | libc++ 729 | CLANG_WARN_CONSTANT_CONVERSION 730 | YES 731 | CLANG_WARN_EMPTY_BODY 732 | YES 733 | CLANG_WARN_ENUM_CONVERSION 734 | YES 735 | CLANG_WARN_INT_CONVERSION 736 | YES 737 | CLANG_WARN__DUPLICATE_METHOD_MATCH 738 | YES 739 | COPY_PHASE_STRIP 740 | NO 741 | GCC_C_LANGUAGE_STANDARD 742 | gnu99 743 | GCC_DYNAMIC_NO_PIC 744 | NO 745 | GCC_ENABLE_OBJC_EXCEPTIONS 746 | YES 747 | GCC_OPTIMIZATION_LEVEL 748 | 0 749 | GCC_PREPROCESSOR_DEFINITIONS 750 | 751 | DEBUG=1 752 | $(inherited) 753 | 754 | GCC_SYMBOLS_PRIVATE_EXTERN 755 | NO 756 | GCC_WARN_64_TO_32_BIT_CONVERSION 757 | YES 758 | GCC_WARN_ABOUT_RETURN_TYPE 759 | YES 760 | GCC_WARN_UNINITIALIZED_AUTOS 761 | YES 762 | GCC_WARN_UNUSED_VARIABLE 763 | YES 764 | MACOSX_DEPLOYMENT_TARGET 765 | 10.8 766 | ONLY_ACTIVE_ARCH 767 | YES 768 | SDKROOT 769 | macosx 770 | 771 | isa 772 | XCBuildConfiguration 773 | name 774 | Debug 775 | 776 | BFD3EB9217DA869400C84390 777 | 778 | buildSettings 779 | 780 | ALWAYS_SEARCH_USER_PATHS 781 | NO 782 | CLANG_CXX_LANGUAGE_STANDARD 783 | gnu++0x 784 | CLANG_CXX_LIBRARY 785 | libc++ 786 | CLANG_WARN_CONSTANT_CONVERSION 787 | YES 788 | CLANG_WARN_EMPTY_BODY 789 | YES 790 | CLANG_WARN_ENUM_CONVERSION 791 | YES 792 | CLANG_WARN_INT_CONVERSION 793 | YES 794 | CLANG_WARN__DUPLICATE_METHOD_MATCH 795 | YES 796 | COPY_PHASE_STRIP 797 | YES 798 | DEBUG_INFORMATION_FORMAT 799 | dwarf-with-dsym 800 | GCC_C_LANGUAGE_STANDARD 801 | gnu99 802 | GCC_ENABLE_OBJC_EXCEPTIONS 803 | YES 804 | GCC_WARN_64_TO_32_BIT_CONVERSION 805 | YES 806 | GCC_WARN_ABOUT_RETURN_TYPE 807 | YES 808 | GCC_WARN_UNINITIALIZED_AUTOS 809 | YES 810 | GCC_WARN_UNUSED_VARIABLE 811 | YES 812 | MACOSX_DEPLOYMENT_TARGET 813 | 10.8 814 | SDKROOT 815 | macosx 816 | 817 | isa 818 | XCBuildConfiguration 819 | name 820 | Release 821 | 822 | BFD3EBA017DA884400C84390 823 | 824 | buildActionMask 825 | 2147483647 826 | files 827 | 828 | BFD3EBAB17DA884400C84390 829 | BFD3EBB017DA884400C84390 830 | BFD3EBC517DA97F700C84390 831 | BFD3EBC817DA980600C84390 832 | 833 | isa 834 | PBXSourcesBuildPhase 835 | runOnlyForDeploymentPostprocessing 836 | 0 837 | 838 | BFD3EBA117DA884400C84390 839 | 840 | buildActionMask 841 | 2147483647 842 | files 843 | 844 | BFD3EBA617DA884400C84390 845 | BFD3EBA817DA884400C84390 846 | 89502BC7CC8045B4A1E1CF78 847 | 848 | isa 849 | PBXFrameworksBuildPhase 850 | runOnlyForDeploymentPostprocessing 851 | 0 852 | 853 | BFD3EBA217DA884400C84390 854 | 855 | buildActionMask 856 | 2147483647 857 | dstPath 858 | /usr/share/man/man1/ 859 | dstSubfolderSpec 860 | 0 861 | files 862 | 863 | isa 864 | PBXCopyFilesBuildPhase 865 | runOnlyForDeploymentPostprocessing 866 | 1 867 | 868 | BFD3EBA317DA884400C84390 869 | 870 | buildConfigurationList 871 | BFD3EBB317DA884400C84390 872 | buildPhases 873 | 874 | 4C1EF852DA514B64B76DE949 875 | BFD3EBA017DA884400C84390 876 | BFD3EBA117DA884400C84390 877 | BFD3EBA217DA884400C84390 878 | 09545A77AF5E431CB18C6E85 879 | 880 | buildRules 881 | 882 | dependencies 883 | 884 | isa 885 | PBXNativeTarget 886 | name 887 | Example 888 | productName 889 | Example 890 | productReference 891 | BFD3EBA417DA884400C84390 892 | productType 893 | com.apple.product-type.tool 894 | 895 | BFD3EBA417DA884400C84390 896 | 897 | explicitFileType 898 | compiled.mach-o.executable 899 | includeInIndex 900 | 0 901 | isa 902 | PBXFileReference 903 | path 904 | Example 905 | sourceTree 906 | BUILT_PRODUCTS_DIR 907 | 908 | BFD3EBA517DA884400C84390 909 | 910 | isa 911 | PBXFileReference 912 | lastKnownFileType 913 | wrapper.framework 914 | name 915 | CoreData.framework 916 | path 917 | System/Library/Frameworks/CoreData.framework 918 | sourceTree 919 | SDKROOT 920 | 921 | BFD3EBA617DA884400C84390 922 | 923 | fileRef 924 | BFD3EBA517DA884400C84390 925 | isa 926 | PBXBuildFile 927 | 928 | BFD3EBA717DA884400C84390 929 | 930 | isa 931 | PBXFileReference 932 | lastKnownFileType 933 | wrapper.framework 934 | name 935 | Foundation.framework 936 | path 937 | System/Library/Frameworks/Foundation.framework 938 | sourceTree 939 | SDKROOT 940 | 941 | BFD3EBA817DA884400C84390 942 | 943 | fileRef 944 | BFD3EBA717DA884400C84390 945 | isa 946 | PBXBuildFile 947 | 948 | BFD3EBA917DA884400C84390 949 | 950 | children 951 | 952 | BFD3EBC217DA97E500C84390 953 | BFD3EBAA17DA884400C84390 954 | BFD3EBAE17DA884400C84390 955 | BFD3EBAC17DA884400C84390 956 | 957 | isa 958 | PBXGroup 959 | name 960 | Example 961 | path 962 | ../Example 963 | sourceTree 964 | <group> 965 | 966 | BFD3EBAA17DA884400C84390 967 | 968 | isa 969 | PBXFileReference 970 | lastKnownFileType 971 | sourcecode.c.objc 972 | path 973 | main.m 974 | sourceTree 975 | <group> 976 | 977 | BFD3EBAB17DA884400C84390 978 | 979 | fileRef 980 | BFD3EBAA17DA884400C84390 981 | isa 982 | PBXBuildFile 983 | 984 | BFD3EBAC17DA884400C84390 985 | 986 | children 987 | 988 | BFD3EBAD17DA884400C84390 989 | 990 | isa 991 | PBXGroup 992 | name 993 | Supporting Files 994 | sourceTree 995 | <group> 996 | 997 | BFD3EBAD17DA884400C84390 998 | 999 | isa 1000 | PBXFileReference 1001 | lastKnownFileType 1002 | sourcecode.c.h 1003 | path 1004 | Example-Prefix.pch 1005 | sourceTree 1006 | <group> 1007 | 1008 | BFD3EBAE17DA884400C84390 1009 | 1010 | children 1011 | 1012 | BFD3EBAF17DA884400C84390 1013 | 1014 | currentVersion 1015 | BFD3EBAF17DA884400C84390 1016 | isa 1017 | XCVersionGroup 1018 | path 1019 | Example.xcdatamodeld 1020 | sourceTree 1021 | <group> 1022 | versionGroupType 1023 | wrapper.xcdatamodel 1024 | 1025 | BFD3EBAF17DA884400C84390 1026 | 1027 | isa 1028 | PBXFileReference 1029 | lastKnownFileType 1030 | wrapper.xcdatamodel 1031 | path 1032 | Example.xcdatamodel 1033 | sourceTree 1034 | <group> 1035 | 1036 | BFD3EBB017DA884400C84390 1037 | 1038 | fileRef 1039 | BFD3EBAE17DA884400C84390 1040 | isa 1041 | PBXBuildFile 1042 | 1043 | BFD3EBB317DA884400C84390 1044 | 1045 | buildConfigurations 1046 | 1047 | BFD3EBB417DA884400C84390 1048 | BFD3EBB517DA884400C84390 1049 | 1050 | defaultConfigurationIsVisible 1051 | 0 1052 | defaultConfigurationName 1053 | Release 1054 | isa 1055 | XCConfigurationList 1056 | 1057 | BFD3EBB417DA884400C84390 1058 | 1059 | baseConfigurationReference 1060 | 68996754EACCFF5C951B1AB7 1061 | buildSettings 1062 | 1063 | CLANG_ENABLE_OBJC_ARC 1064 | YES 1065 | GCC_PRECOMPILE_PREFIX_HEADER 1066 | YES 1067 | GCC_PREFIX_HEADER 1068 | ../Example/Example-Prefix.pch 1069 | PRODUCT_NAME 1070 | $(TARGET_NAME) 1071 | 1072 | isa 1073 | XCBuildConfiguration 1074 | name 1075 | Debug 1076 | 1077 | BFD3EBB517DA884400C84390 1078 | 1079 | baseConfigurationReference 1080 | D0F2472B42C44AC0544C862D 1081 | buildSettings 1082 | 1083 | CLANG_ENABLE_OBJC_ARC 1084 | YES 1085 | GCC_PRECOMPILE_PREFIX_HEADER 1086 | YES 1087 | GCC_PREFIX_HEADER 1088 | ../Example/Example-Prefix.pch 1089 | PRODUCT_NAME 1090 | $(TARGET_NAME) 1091 | 1092 | isa 1093 | XCBuildConfiguration 1094 | name 1095 | Release 1096 | 1097 | BFD3EBBD17DA95F300C84390 1098 | 1099 | buildConfigurationList 1100 | BFD3EBBE17DA95F300C84390 1101 | buildPhases 1102 | 1103 | BFD3EBC117DA95FC00C84390 1104 | 1105 | dependencies 1106 | 1107 | isa 1108 | PBXAggregateTarget 1109 | name 1110 | mogenerator 1111 | productName 1112 | mogenerator 1113 | 1114 | BFD3EBBE17DA95F300C84390 1115 | 1116 | buildConfigurations 1117 | 1118 | BFD3EBBF17DA95F300C84390 1119 | BFD3EBC017DA95F300C84390 1120 | 1121 | defaultConfigurationIsVisible 1122 | 0 1123 | defaultConfigurationName 1124 | Release 1125 | isa 1126 | XCConfigurationList 1127 | 1128 | BFD3EBBF17DA95F300C84390 1129 | 1130 | buildSettings 1131 | 1132 | PRODUCT_NAME 1133 | $(TARGET_NAME) 1134 | 1135 | isa 1136 | XCBuildConfiguration 1137 | name 1138 | Debug 1139 | 1140 | BFD3EBC017DA95F300C84390 1141 | 1142 | buildSettings 1143 | 1144 | PRODUCT_NAME 1145 | $(TARGET_NAME) 1146 | 1147 | isa 1148 | XCBuildConfiguration 1149 | name 1150 | Release 1151 | 1152 | BFD3EBC117DA95FC00C84390 1153 | 1154 | buildActionMask 1155 | 2147483647 1156 | files 1157 | 1158 | inputPaths 1159 | 1160 | isa 1161 | PBXShellScriptBuildPhase 1162 | name 1163 | mogenerator 1164 | outputPaths 1165 | 1166 | runOnlyForDeploymentPostprocessing 1167 | 0 1168 | shellPath 1169 | /bin/sh 1170 | shellScript 1171 | export PATH=$PATH:/usr/local/bin 1172 | mogenerator -m "../Example/Example.xcdatamodeld" --template-var arc=true -O ../Example/ 1173 | 1174 | 1175 | BFD3EBC217DA97E500C84390 1176 | 1177 | children 1178 | 1179 | BFD3EBC317DA97F700C84390 1180 | BFD3EBC417DA97F700C84390 1181 | BFD3EBC617DA980600C84390 1182 | BFD3EBC717DA980600C84390 1183 | 1184 | isa 1185 | PBXGroup 1186 | name 1187 | Core Data classes 1188 | sourceTree 1189 | <group> 1190 | 1191 | BFD3EBC317DA97F700C84390 1192 | 1193 | fileEncoding 1194 | 4 1195 | isa 1196 | PBXFileReference 1197 | lastKnownFileType 1198 | sourcecode.c.h 1199 | path 1200 | _EntityWithCustomClass.h 1201 | sourceTree 1202 | <group> 1203 | 1204 | BFD3EBC417DA97F700C84390 1205 | 1206 | fileEncoding 1207 | 4 1208 | isa 1209 | PBXFileReference 1210 | lastKnownFileType 1211 | sourcecode.c.objc 1212 | path 1213 | _EntityWithCustomClass.m 1214 | sourceTree 1215 | <group> 1216 | 1217 | BFD3EBC517DA97F700C84390 1218 | 1219 | fileRef 1220 | BFD3EBC417DA97F700C84390 1221 | isa 1222 | PBXBuildFile 1223 | 1224 | BFD3EBC617DA980600C84390 1225 | 1226 | fileEncoding 1227 | 4 1228 | isa 1229 | PBXFileReference 1230 | lastKnownFileType 1231 | sourcecode.c.h 1232 | path 1233 | EntityWithCustomClass.h 1234 | sourceTree 1235 | <group> 1236 | 1237 | BFD3EBC717DA980600C84390 1238 | 1239 | fileEncoding 1240 | 4 1241 | isa 1242 | PBXFileReference 1243 | lastKnownFileType 1244 | sourcecode.c.objc 1245 | path 1246 | EntityWithCustomClass.m 1247 | sourceTree 1248 | <group> 1249 | 1250 | BFD3EBC817DA980600C84390 1251 | 1252 | fileRef 1253 | BFD3EBC717DA980600C84390 1254 | isa 1255 | PBXBuildFile 1256 | 1257 | BFE657271831D6FF00D8C601 1258 | 1259 | fileRef 1260 | BFD3EBAE17DA884400C84390 1261 | isa 1262 | PBXBuildFile 1263 | 1264 | BFE657281831D70700D8C601 1265 | 1266 | fileRef 1267 | BFD3EBC417DA97F700C84390 1268 | isa 1269 | PBXBuildFile 1270 | 1271 | BFE657291831D70700D8C601 1272 | 1273 | fileRef 1274 | BFD3EBC717DA980600C84390 1275 | isa 1276 | PBXBuildFile 1277 | 1278 | D0F2472B42C44AC0544C862D 1279 | 1280 | includeInIndex 1281 | 1 1282 | isa 1283 | PBXFileReference 1284 | lastKnownFileType 1285 | text.xcconfig 1286 | name 1287 | Pods.release.xcconfig 1288 | path 1289 | Pods/Target Support Files/Pods/Pods.release.xcconfig 1290 | sourceTree 1291 | <group> 1292 | 1293 | 1294 | rootObject 1295 | BFD3EB7A17DA869400C84390 1296 | 1297 | 1298 | -------------------------------------------------------------------------------- /Project/GDCoreDataConcurrencyDebugging.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Project/GDCoreDataConcurrencyDebugging.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Project/GDCoreDataConcurrencyDebugging.xcworkspace/xcshareddata/GDCoreDataConcurrencyDebugging.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 3C4E663F-28BC-4EC0-8003-15C560A17C2B 9 | IDESourceControlProjectName 10 | GDCoreDataConcurrencyDebugging 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 7EC66276110F56261149A95B426589703EED10F8 14 | https://github.com/facebook/fishhook.git 15 | F48369D226C1084E2106B0954C2391A75187088E 16 | ssh://github.com/GrahamDennis/GDCoreDataConcurrencyDebugging.git 17 | 18 | IDESourceControlProjectPath 19 | Project/GDCoreDataConcurrencyDebugging.xcworkspace 20 | IDESourceControlProjectRelativeInstallPathDictionary 21 | 22 | 7EC66276110F56261149A95B426589703EED10F8 23 | ../../Vendor/fishhook 24 | F48369D226C1084E2106B0954C2391A75187088E 25 | ../.. 26 | 27 | IDESourceControlProjectURL 28 | ssh://github.com/GrahamDennis/GDCoreDataConcurrencyDebugging.git 29 | IDESourceControlProjectVersion 30 | 111 31 | IDESourceControlProjectWCCIdentifier 32 | F48369D226C1084E2106B0954C2391A75187088E 33 | IDESourceControlProjectWCConfigurations 34 | 35 | 36 | IDESourceControlRepositoryExtensionIdentifierKey 37 | public.vcs.git 38 | IDESourceControlWCCIdentifierKey 39 | 7EC66276110F56261149A95B426589703EED10F8 40 | IDESourceControlWCCName 41 | fishhook 42 | 43 | 44 | IDESourceControlRepositoryExtensionIdentifierKey 45 | public.vcs.git 46 | IDESourceControlWCCIdentifierKey 47 | F48369D226C1084E2106B0954C2391A75187088E 48 | IDESourceControlWCCName 49 | GDCoreDataConcurrencyDebugging 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Project/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | pod "GDCoreDataConcurrencyDebugging", :path => ".." 4 | 5 | link_with ['Example', 'Tests'] -------------------------------------------------------------------------------- /Project/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - GDCoreDataConcurrencyDebugging (0.1): 3 | - JRSwizzle 4 | - JRSwizzle (1.0) 5 | 6 | DEPENDENCIES: 7 | - GDCoreDataConcurrencyDebugging (from `..`) 8 | 9 | EXTERNAL SOURCES: 10 | GDCoreDataConcurrencyDebugging: 11 | :path: .. 12 | 13 | SPEC CHECKSUMS: 14 | GDCoreDataConcurrencyDebugging: 484fb925228b5beac19774c21e4db9ed1f9cc564 15 | JRSwizzle: 30da7a2c539a4ebf954b16d15f6dd8221c9f6fa5 16 | 17 | COCOAPODS: 0.34.4 18 | -------------------------------------------------------------------------------- /Project/Tests/CoreDataTestHelpers.h: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataTestHelpers.h 3 | // GDCoreDataConcurrencyDebugging 4 | // 5 | // Created by Graham Dennis on 12/11/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NSManagedObjectContext *managedObjectContext(); 12 | 13 | extern NSCountedSet *ConcurrencyFailures; 14 | 15 | @interface CoreDataTestHelpers : NSObject 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Project/Tests/CoreDataTestHelpers.m: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataTestHelpers.m 3 | // GDCoreDataConcurrencyDebugging 4 | // 5 | // Created by Graham Dennis on 12/11/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import "CoreDataTestHelpers.h" 10 | #import 11 | 12 | NSManagedObjectModel *managedObjectModel() 13 | { 14 | static NSManagedObjectModel *model = nil; 15 | if (model != nil) { 16 | return model; 17 | } 18 | 19 | NSURL *modelURL = [[NSBundle bundleForClass:[CoreDataTestHelpers class]] URLForResource:@"Example" withExtension:@"momd"]; 20 | model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; 21 | 22 | return model; 23 | } 24 | 25 | NSManagedObjectContext *managedObjectContext() 26 | { 27 | NSManagedObjectContext *context = nil; 28 | if (context != nil) { 29 | return context; 30 | } 31 | 32 | @autoreleasepool { 33 | context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; 34 | 35 | NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel()]; 36 | [context setPersistentStoreCoordinator:coordinator]; 37 | 38 | NSString *STORE_TYPE = NSInMemoryStoreType; 39 | NSURL *storeURL = [NSURL fileURLWithPath:@"/tmp/foo.sqlite"]; 40 | 41 | NSError *error; 42 | NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE configuration:nil URL:storeURL options:nil error:&error]; 43 | 44 | if (newStore == nil) { 45 | NSLog(@"Store Configuration Failure %@", ([error localizedDescription] != nil) ? [error localizedDescription] : @"Unknown Error"); 46 | } 47 | } 48 | return context; 49 | } 50 | 51 | NSCountedSet *ConcurrencyFailures = nil; 52 | 53 | void ConcurrencyFailure(SEL _cmd) 54 | { 55 | static dispatch_queue_t access_queue; 56 | static dispatch_once_t onceToken; 57 | dispatch_once(&onceToken, ^{ 58 | access_queue = dispatch_queue_create("me.grahamdennis.GDCoreDataConcurrencyDebugging.Tests", DISPATCH_QUEUE_SERIAL); 59 | }); 60 | 61 | dispatch_sync(access_queue, ^{ 62 | [ConcurrencyFailures addObject:NSStringFromSelector(_cmd)]; 63 | }); 64 | } 65 | 66 | @implementation CoreDataTestHelpers 67 | 68 | + (void)load 69 | { 70 | GDCoreDataConcurrencyDebuggingSetFailureHandler(ConcurrencyFailure); 71 | ConcurrencyFailures = [NSCountedSet new]; 72 | } 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /Project/Tests/ObjectArrayTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectArrayTests.m 3 | // GDCoreDataConcurrencyDebugging 4 | // 5 | // Created by Graham Dennis on 12/11/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CoreDataTestHelpers.h" 11 | #import "EntityWithCustomClass.h" 12 | 13 | @interface ObjectArrayTests : XCTestCase 14 | 15 | @property (nonatomic, strong) NSManagedObjectContext *context; 16 | @property (nonatomic, copy) NSArray *objects; 17 | 18 | @end 19 | 20 | @implementation ObjectArrayTests 21 | 22 | - (void)setUp 23 | { 24 | [super setUp]; 25 | // Put setup code here. This method is called before the invocation of each test method in the class. 26 | 27 | self.context = managedObjectContext(); 28 | 29 | [self.context performBlockAndWait:^{ 30 | @autoreleasepool { 31 | 32 | for (NSString *name in @[@"a", @"b", @"c"]) { 33 | EntityWithCustomClass *object = [EntityWithCustomClass insertInManagedObjectContext:self.context]; 34 | object.name = name; 35 | } 36 | 37 | NSFetchRequest *fetchRequest = [NSFetchRequest new]; 38 | fetchRequest.entity = [EntityWithCustomClass entityInManagedObjectContext:self.context]; 39 | fetchRequest.includesPendingChanges = YES; 40 | NSArray *tempResults = [self.context executeFetchRequest:fetchRequest error:NULL]; 41 | 42 | self.objects = [[tempResults mutableCopy] copy]; // We can be sure that 'objects' is not a magic CoreData NSArray 43 | } 44 | }]; 45 | 46 | [ConcurrencyFailures removeAllObjects]; 47 | } 48 | 49 | - (void)tearDown 50 | { 51 | // Put teardown code here. This method is called after the invocation of each test method in the class. 52 | [super tearDown]; 53 | 54 | [self.context performBlockAndWait:^{ 55 | self.objects = nil; 56 | }]; 57 | } 58 | 59 | // With optimisations turned off, the compiler will generate an autorelease method for this method 60 | NSManagedObject *IdentityFunction(NSManagedObject *object) 61 | { 62 | return object; 63 | } 64 | 65 | - (void)testUnsafeRelease 66 | { 67 | self.objects = nil; 68 | 69 | NSCountedSet *expectedFailures = [NSCountedSet setWithArray:@[@"release", @"release", @"release"]]; 70 | 71 | XCTAssertEqualObjects(ConcurrencyFailures, expectedFailures, @"Incorrect number of release messages"); 72 | } 73 | 74 | - (void)testSafeAccess 75 | { 76 | @autoreleasepool { 77 | // This code is safe because we are just calling -objectID 78 | // But it's only safe as long as the context isn't reset (or deallocated) before the autorelease pool pops. 79 | NSMutableArray *objectIDs = [NSMutableArray new]; 80 | for (EntityWithCustomClass *object in self.objects) { 81 | [objectIDs addObject:object.objectID]; 82 | } 83 | } 84 | 85 | XCTAssertEqualObjects(ConcurrencyFailures, [NSCountedSet set], @"This should be safe"); 86 | } 87 | 88 | - (void)testNestedContextSave 89 | { 90 | NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; 91 | childContext.parentContext = self.context; 92 | 93 | [childContext performBlockAndWait:^{ 94 | @autoreleasepool { 95 | EntityWithCustomClass *object = [EntityWithCustomClass insertInManagedObjectContext:childContext]; 96 | object.name = @"test"; 97 | 98 | [childContext save:NULL]; 99 | } 100 | }]; 101 | 102 | XCTAssertEqualObjects(ConcurrencyFailures, [NSCountedSet set], @"This should be safe"); 103 | } 104 | 105 | @end 106 | -------------------------------------------------------------------------------- /Project/Tests/Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | me.grahamdennis.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Project/Tests/Tests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #endif 10 | -------------------------------------------------------------------------------- /Project/Tests/Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.m 3 | // Tests 4 | // 5 | // Created by Graham Dennis on 11/11/13. 6 | // Copyright (c) 2013 Graham Dennis. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CoreDataTestHelpers.h" 11 | #import 12 | #import "EntityWithCustomClass.h" 13 | 14 | @interface Tests : XCTestCase 15 | 16 | @end 17 | 18 | @interface NSManagedObject (EntityAccessors) 19 | 20 | @property (nonatomic, strong) NSString *name; 21 | 22 | @end 23 | 24 | 25 | 26 | @implementation Tests 27 | 28 | - (void)setUp 29 | { 30 | [super setUp]; 31 | // Put setup code here. This method is called before the invocation of each test method in the class. 32 | 33 | [ConcurrencyFailures removeAllObjects]; 34 | } 35 | 36 | - (void)tearDown 37 | { 38 | // Put teardown code here. This method is called after the invocation of each test method in the class. 39 | [super tearDown]; 40 | } 41 | 42 | - (void)testSimple 43 | { 44 | NSManagedObjectContext *context = managedObjectContext(); 45 | 46 | NSEntityDescription *entity = [NSEntityDescription entityForName:@"Entity" inManagedObjectContext:context]; 47 | 48 | __block NSManagedObject *objectInContext1 = nil; 49 | [context performBlockAndWait:^{ 50 | objectInContext1 = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context]; 51 | objectInContext1.name = @"test"; 52 | [context save:NULL]; 53 | }]; 54 | 55 | // Here's an obvious invalid access 56 | NSString *__unused name = objectInContext1.name; 57 | 58 | XCTAssertEqualObjects(ConcurrencyFailures, [NSCountedSet setWithArray:@[@"willAccessValueForKey:"]], @"Missed concurrency failure"); 59 | } 60 | 61 | 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /Project/Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDCoreDataConcurrencyDebugging 2 | 3 | [![Version](http://cocoapod-badges.herokuapp.com/v/GDCoreDataConcurrencyDebugging/badge.png)](http://cocoadocs.org/docsets/GDCoreDataConcurrencyDebugging) 4 | [![Platform](http://cocoapod-badges.herokuapp.com/p/GDCoreDataConcurrencyDebugging/badge.png)](http://cocoadocs.org/docsets/GDCoreDataConcurrencyDebugging) 5 | 6 | GDCoreDataConcurrencyDebugging helps you find cases where NSManagedObject's are being called on the wrong thread or dispatch queue. Simply add it to your project and you will get a log message for every invalid access to an NSManagedObject. 7 | 8 | For example the following code will trigger a console message: 9 | 10 | __block NSManagedObject *objectInContext1 = nil; 11 | [context1 performBlockAndWait:^{ 12 | objectInContext1 = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context1]; 13 | objectInContext1.name = @"test"; 14 | [context1 save:NULL]; 15 | }]; 16 | 17 | // Invalid access 18 | NSString *name = objectInContext1.name; 19 | 20 | 21 | If you want to customise the logging you can call `GDCoreDataConcurrencyDebuggingSetFailureHandler` to set your own concurrency failure handler with function prototype `void ConcurrencyFailureHandler(SEL _cmd);`. For example: 22 | 23 | #import 24 | 25 | static void CoreDataConcurrencyFailureHandler(SEL _cmd) 26 | { 27 | // Simply checking _cmd == @selector(autorelease) won't work in ARC code. 28 | // You really shouldn't ignore -autorelease messages sent on the wrong thread, 29 | // but if you want to live on the wild side... 30 | if (_cmd == NSSelectorFromString(@"autorelease")) return; 31 | NSLog(@"CoreData concurrency failure: Selector '%@' called on wrong queue/thread.", NSStringFromSelector(_cmd)); 32 | } 33 | 34 | int main() 35 | { 36 | GDCoreDataConcurrencyDebuggingSetFailureHandler(CoreDataConcurrencyFailureHandler); 37 | } 38 | 39 | See my [blog post][blog-post] for more information. 40 | 41 | ## Usage 42 | 43 | To run the example project; clone the repo, and run `pod install` from the Project directory first. The example demonstrates some invalid CoreData code. A particularly nasty case demonstrated is when an autorelease pool pops after the owning `NSManagedObjectContext` has been reset or dealloc'ed. 44 | 45 | ## Requirements 46 | 47 | Mac OS X 10.6+, iOS 3.1+, [JRSwizzle] 48 | 49 | ## Installation 50 | 51 | GDCoreDataConcurrencyDebugging is available through [CocoaPods](http://cocoapods.org), to install 52 | it simply add the following line to your Podfile: 53 | 54 | pod "GDCoreDataConcurrencyDebugging" 55 | 56 | If you're installing manually, be sure to make sure ARC is turned off for the GDCoreDataConcurrencyDebugging sources (use the `-fno-objc-arc` flag). GDCoreDataConcurrencyDebugging can be safely linked against ARC code. See the Example. 57 | 58 | ## How does it work? 59 | 60 | GDCoreDataConcurrencyDebugging uses dynamic subclassing to create a custom `NSManagedObject` subclass which tracks access to instance variables and when they are modified. Note that GDCoreDataConcurrencyDebugging does not check that CoreData faulting collections (used for relationships) are accessed correctly after they have been retrieved from an NSManagedObject. 61 | 62 | GDCoreDataConcurrencyDebugging is based on Mike Ash's dynamic subclassing code in [MAZeroingWeakRef]. 63 | 64 | ## Author 65 | 66 | Graham Dennis, graham@grahamdennis.me 67 | 68 | 69 | 70 | ## License 71 | 72 | GDCoreDataConcurrencyDebugging is available under the MIT license. See the LICENSE file for more info. 73 | 74 | 75 | [MAZeroingWeakRef]: https://github.com/mikeash/MAZeroingWeakRef 76 | [blog-post]: http://www.grahamdennis.me/blog/2013/09/09/debugging-concurrency-with-core-data/ 77 | [JRSwizzle]: https://github.com/rentzsch/jrswizzle -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "Runs the specs [EMPTY]" 2 | task :spec do 3 | # Provide your own implementation 4 | end 5 | 6 | task :version do 7 | git_remotes = `git remote`.strip.split("\n") 8 | 9 | if git_remotes.count > 0 10 | puts "-- fetching version number from github" 11 | sh 'git fetch' 12 | 13 | remote_version = remote_spec_version 14 | end 15 | 16 | if remote_version.nil? 17 | puts "There is no current released version. You're about to release a new Pod." 18 | version = "0.0.1" 19 | else 20 | puts "The current released version of your pod is " + remote_spec_version.to_s() 21 | version = suggested_version_number 22 | end 23 | 24 | puts "Enter the version you want to release (" + version + ") " 25 | new_version_number = $stdin.gets.strip 26 | if new_version_number == "" 27 | new_version_number = version 28 | end 29 | 30 | replace_version_number(new_version_number) 31 | end 32 | 33 | desc "Release a new version of the Pod" 34 | task :release do 35 | 36 | puts "* Running version" 37 | sh "rake version" 38 | 39 | unless ENV['SKIP_CHECKS'] 40 | if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' 41 | $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." 42 | exit 1 43 | end 44 | 45 | if `git tag`.strip.split("\n").include?(spec_version) 46 | $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" 47 | exit 1 48 | end 49 | 50 | puts "You are about to release `#{spec_version}`, is that correct? [y/n]" 51 | exit if $stdin.gets.strip.downcase != 'y' 52 | end 53 | 54 | puts "* Running specs" 55 | sh "rake spec" 56 | 57 | puts "* Linting the podspec" 58 | sh "pod lib lint" 59 | 60 | # Then release 61 | sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}'" 62 | sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" 63 | sh "git push origin master" 64 | sh "git push origin --tags" 65 | sh "pod push master #{podspec_path}" 66 | end 67 | 68 | # @return [Pod::Version] The version as reported by the Podspec. 69 | # 70 | def spec_version 71 | require 'cocoapods' 72 | spec = Pod::Specification.from_file(podspec_path) 73 | spec.version 74 | end 75 | 76 | # @return [Pod::Version] The version as reported by the Podspec from remote. 77 | # 78 | def remote_spec_version 79 | require 'cocoapods-core' 80 | 81 | if spec_file_exist_on_remote? 82 | remote_spec = eval(`git show origin/master:#{podspec_path}`) 83 | remote_spec.version 84 | else 85 | nil 86 | end 87 | end 88 | 89 | # @return [Bool] If the remote repository has a copy of the podpesc file or not. 90 | # 91 | def spec_file_exist_on_remote? 92 | test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; 93 | then 94 | echo 'true' 95 | else 96 | echo 'false' 97 | fi` 98 | 99 | 'true' == test_condition.strip 100 | end 101 | 102 | # @return [String] The relative path of the Podspec. 103 | # 104 | def podspec_path 105 | podspecs = Dir.glob('*.podspec') 106 | if podspecs.count == 1 107 | podspecs.first 108 | else 109 | raise "Could not select a podspec" 110 | end 111 | end 112 | 113 | # @return [String] The suggested version number based on the local and remote version numbers. 114 | # 115 | def suggested_version_number 116 | if spec_version != remote_spec_version 117 | spec_version.to_s() 118 | else 119 | next_version(spec_version).to_s() 120 | end 121 | end 122 | 123 | # @param [Pod::Version] version 124 | # the version for which you need the next version 125 | # 126 | # @note It is computed by bumping the last component of the versino string by 1. 127 | # 128 | # @return [Pod::Version] The version that comes next after the version supplied. 129 | # 130 | def next_version(version) 131 | version_components = version.to_s().split("."); 132 | last = (version_components.last.to_i() + 1).to_s 133 | version_components[-1] = last 134 | Pod::Version.new(version_components.join(".")) 135 | end 136 | 137 | # @param [String] new_version_number 138 | # the new version number 139 | # 140 | # @note This methods replaces the version number in the podspec file with a new version number. 141 | # 142 | # @return void 143 | # 144 | def replace_version_number(new_version_number) 145 | text = File.read(podspec_path) 146 | text.gsub!(/(s.version( )*= ")#{spec_version}(")/, "\\1#{new_version_number}\\3") 147 | File.open(podspec_path, "w") { |file| file.puts text } 148 | end --------------------------------------------------------------------------------