diff --git a/Circle/CircleIVarLayout.h b/Circle/CircleIVarLayout.h index 10bf827..cfd7455 100644 --- a/Circle/CircleIVarLayout.h +++ b/Circle/CircleIVarLayout.h @@ -9,4 +9,8 @@ #import +// Enumerate the strong references in a given object, calling the block for each one found. +// The block's reference parameter may be NULL, in cases where the location of the strong +// reference cannot be determined (for example, when providing references pulled from +// Cocoa collections. void EnumerateStrongReferences(void *obj, void (^block)(void **reference, void *target)); diff --git a/Circle/CircleIVarLayout.m b/Circle/CircleIVarLayout.m index 98ef7d7..d65821f 100644 --- a/Circle/CircleIVarLayout.m +++ b/Circle/CircleIVarLayout.m @@ -11,6 +11,7 @@ #import +// Blocks runtime structures and constants struct BlockDescriptor { unsigned long reserved; @@ -36,6 +37,9 @@ }; +// In order to know how to scan an object, the code needs to know what kind of object it is. +// Most objects (OTHER) get scanned by invoking the ARC destructor. Blocks are scanned by +// invoking the block destructor. Cocoa collections are scanned by enumerating them. enum Classification { ENUMERABLE, @@ -44,10 +48,16 @@ OTHER }; + +// Dictionarys to cache the layout and classification of a class. static CFMutableDictionaryRef gLayoutCache; static CFMutableDictionaryRef gClassificationCache; +// This class detects releases sent to it and makes that information available to +// the outside. It's used to detect strong references by watching which ivar slots +// are released by the ARC/block destructor. It also makes a weak attempt to imitate +// a block byref structure to keep block destructors from crashing on byref slots. struct _block_byref_block; @interface _CircleReleaseDetector : NSObject { // __block fakery @@ -62,6 +72,7 @@ @interface _CircleReleaseDetector : NSObject { BOOL _didRelease; } +// We deal in void * here because we can't let ARC do any sort of memory management. + (void *)make; // And just free() the result when done static BOOL DidRelease(void *obj); @@ -72,11 +83,13 @@ static void byref_keep_nop(struct _block_byref_block *dst, struct _block_byref_b static void byref_dispose_nop(struct _block_byref_block *param) {} + (void)initialize { + // Swizzle out -release, since ARC doesn't let us override it directly. Method m = class_getInstanceMethod(self, @selector(release_toSwizzle)); class_addMethod(self, sel_getUid("release"), method_getImplementation(m), method_getTypeEncoding(m)); } + (void *)make { + // Allocate memory manually to ensure ARC doesn't cause any trouble. void *memory = calloc(class_getInstanceSize(self), 1); __unsafe_unretained _CircleReleaseDetector *obj = (__bridge __unsafe_unretained id)memory; object_setClass(obj, self); @@ -96,28 +109,49 @@ static BOOL DidRelease(void *obj) { @end +// Determine whether a given object is a block. static BOOL IsBlock(void *obj) { + // Create a known block, then find the topmost superclass that isn't NSObject. + // We assume that this topmost class is the topmost block class. Class blockClass = [[^{ NSLog(@"%p", obj); } copy] class]; while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) blockClass = class_getSuperclass(blockClass); + // If the object is an instance of the block class, then it's a block. Otherwise not. Class candidate = object_getClass((__bridge id)obj); return [candidate isSubclassOfClass: blockClass]; } +// Calculate the layout of strong ivars for an object with a given isa, class, and destructor. static NSIndexSet *CalculateStrongLayout(void *isa, size_t objSize, void(^destruct)(void *fakeObj)) { + // We need to know how big pointers are so we can figure out how much memory to allocate. + // We can pretty safely assume that pointer ivars are aligned, but we don't know which + // ivars are pointers and which aren't. If somehow the object size is not a multiple + // of the pointer size, we'll round up, so every slot can be filled. size_t ptrSize = sizeof(void *); + + // Figure out the number of pointers it takes to fill out the object. size_t elements = (objSize + ptrSize - 1) / ptrSize; + + // Create a fake object of the appropriate length. void *obj[elements]; + + // Also create a separate array to track the release detectors so we can check on them after. + // We can't query the contents of 'obj' because the destructor may zero out ivars. void *detectors[elements]; + + // Set up the object. The first slot is the isa, the rest are release detectors. obj[0] = isa; for (size_t i = 0; i < elements; i++) detectors[i] = obj[i] = [_CircleReleaseDetector make]; + // Invoke the destructor. destruct(obj); + // Run through the release detectors and add each one that got released to the object's + // strong ivar layout. While we're at it, free the release detectors. NSMutableIndexSet *layout = [NSMutableIndexSet indexSet]; for (unsigned i = 0; i < elements; i++) { @@ -131,23 +165,32 @@ static BOOL IsBlock(void *obj) static NSIndexSet *GetClassStrongLayout(Class c); +// Calculate the strong ivar layout for a given class. static NSIndexSet *CalculateClassStrongLayout(Class c) { + // Fetch the selector for the ARC destructor. SEL destructorSEL = sel_getUid(".cxx_destruct"); + // Fetch the IMP for the destructor. Also fetch the IMP for a known unimplemented selector. void (*Destruct)(void *, SEL) = (__typeof__(Destruct))class_getMethodImplementation(c, destructorSEL); void (*Forward)(void *, SEL) = (__typeof__(Forward))class_getMethodImplementation([NSObject class], @selector(doNotImplementThisItDoesNotExistReally)); + // If the ARC destructor is not implemented (IMP equals that of an unimplemented selector) + // then the class contains no strong references. We can just bail out now. if(Destruct == Forward) return [NSIndexSet indexSet]; + // Calculate the strong layout for an object with this class as isa, the appropriate size, + // and a destructor that calls the ARC destructor IMP. NSIndexSet *layout = CalculateStrongLayout((__bridge void *)c, class_getInstanceSize(c), ^(void *fakeObj) { Destruct(fakeObj, destructorSEL); }); + // The ARC destructor does not call super. We have to mix in super ivars manually. Class superclass = [c superclass]; if(superclass) { + // Get the strong layout for the superclass, and add its layout to the current one. NSIndexSet *superLayout = GetClassStrongLayout(superclass); NSMutableIndexSet *both = [layout mutableCopy]; [both addIndexes: superLayout]; @@ -156,38 +199,56 @@ static BOOL IsBlock(void *obj) return layout; } +// Fetch the strong ivar layout for a class, pulling from the cache when possible. static NSIndexSet *GetClassStrongLayout(Class c) { + // If the layout cache doesn't exist, create it now. We'll be adding an entry. if(!gLayoutCache) gLayoutCache = CFDictionaryCreateMutable(NULL, 0, NULL, &kCFTypeDictionaryValueCallBacks); + // Fetch the layout from the cache. NSIndexSet *layout = (__bridge NSIndexSet *)CFDictionaryGetValue(gLayoutCache, (__bridge void *)c); + + // If the layout doesn't exist in the cache, then compute it and cache it. if(!layout) { layout = CalculateClassStrongLayout(c); CFDictionarySetValue(gLayoutCache, (__bridge void *)c, (__bridge void *)layout); } + return layout; } - +// Fetch the strong reference layout for a block, pulling from the cache when possible. static NSIndexSet *GetBlockStrongLayout(void *block) { + // We know it's a block here, so we can use the Block structure to access things. struct Block *realBlock = block; + + // If the block doesn't have a destructor then it has no strong references. if(!(realBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return [NSIndexSet indexSet]; + // Global blocks likewise have no strong references. if(realBlock->flags & BLOCK_IS_GLOBAL) return [NSIndexSet indexSet]; + // Otherwise, fetch the block destructor from the block's descriptor. struct BlockDescriptor *descriptor = realBlock->descriptor; void (*dispose_helper)(void *src) = descriptor->rest[1]; + // If the layout cache doesn't exist, create it now. We'll add an entry to it. if(!gLayoutCache) gLayoutCache = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); + // See if the layout already exists. We can't use the block isa as a key, since different + // blocks can share an isa. Instead, we use the address of the destructor function as the + // key, since that destructor will always result in the same layout. NSIndexSet *layout = (__bridge NSIndexSet *)CFDictionaryGetValue(gLayoutCache, dispose_helper); + + // If the layout doesn't exist in the cache, calculate it using this block's isa, the block's + // size as pulled from its descriptor, and a destructor that just calls the block destructor. if(!layout) { layout = CalculateStrongLayout(realBlock->isa, descriptor->size, ^(void *fakeObj) { @@ -195,29 +256,42 @@ static BOOL IsBlock(void *obj) }); CFDictionarySetValue(gLayoutCache, dispose_helper, (__bridge void *)layout); } + return layout; } +// Classify an object into one of the listed classifications. static enum Classification Classify(void *obj) { + // If the classification cache doesn't exist, create it. if(!gClassificationCache) gClassificationCache = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); + // Key classifications off the object's class. void *key = (__bridge void *)object_getClass((__bridge id)obj); + // See if an entry exists in the cache, and return it if it does. const void *value; Boolean present = CFDictionaryGetValueIfPresent(gClassificationCache, key, &value); if(present) return (enum Classification)value; + // Objects are OTHER unless otherwise determined. enum Classification classification = OTHER; + + // Blocks are, well, BLOCK. if(IsBlock(obj)) classification = BLOCK; + + // Arrays and sets are ENUMERABLE. Other NSFastEnumeration classes can be added to this. else if([(__bridge id)obj isKindOfClass: [NSArray class]] || [(__bridge id)obj isKindOfClass: [NSSet class]]) classification = ENUMERABLE; + + // Dictionaries are handled separately, since we have to enumerate keys and objects both. else if([(__bridge id)obj isKindOfClass: [NSDictionary class]]) classification = DICTIONARY; + // Set the computed classification in the cache, then return it. CFDictionarySetValue(gClassificationCache, key, (const void *)classification); return classification; @@ -225,14 +299,17 @@ static enum Classification Classify(void *obj) void EnumerateStrongReferences(void *obj, void (^block)(void **reference, void *target)) { + // How we enumerate strong references depensd on the object's classification. enum Classification classification = Classify(obj); if(classification == ENUMERABLE) { + // ENUMERABLE objects just use NSFastEnumeration. for(id target in (__bridge id)obj) block(NULL, (__bridge void *)target); } else if(classification == DICTIONARY) { + // Dictionaries use the dictionary block enumeration to hit both keys and objects. [(__bridge NSDictionary *)obj enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop) { block(NULL, (__bridge void *)key); block(NULL, (__bridge void *)obj); @@ -240,15 +317,23 @@ void EnumerateStrongReferences(void *obj, void (^block)(void **reference, void * } else { + // Both BLOCK and OTHER use strong ivar layout data, although we have to fetch that layout + // differently depending on the classification. NSIndexSet *layout; if(classification == BLOCK) layout = GetBlockStrongLayout(obj); else layout = GetClassStrongLayout(object_getClass((__bridge id)obj)); + // Treat the object as an array of void * to extract the references void **objAsReferences = obj; [layout enumerateIndexesUsingBlock: ^(NSUInteger idx, BOOL *stop) { + // The reference is pointer #idx in the object. void **reference = &objAsReferences[idx]; + + // The target is just what's located at the reference. + // NOTE: I'm pretty sure this ?: is pointless here, and is held + // over from when this code lived elsewhere. Need to verify before removing. void *target = reference ? *reference : NULL; block(reference, target); }]; diff --git a/Circle/CircleSimpleCycleFinder.h b/Circle/CircleSimpleCycleFinder.h index 7138b3c..8652b99 100644 --- a/Circle/CircleSimpleCycleFinder.h +++ b/Circle/CircleSimpleCycleFinder.h @@ -9,6 +9,9 @@ #import +// A structure used to return results from a cycle search. +// The CF objects are returned retained and must be released +// by the caller. struct CircleSearchResults { BOOL isUnclaimedCycle; @@ -16,24 +19,63 @@ struct CircleSearchResults CFDictionaryRef infos; }; +// Search for a cycle starting from the given object. The object +// is assumed to have a retain count of 1 above what it would +// normally have (so the caller can have it retained after fetching +// it from a __weak variable). +// If gatherAll is false, then the search stops as soon as a cycle is +// known not to be possible and reduced info may be returned. +// If true, the full search is performed regardless, which is useful +// when you're after full object info. struct CircleSearchResults CircleSimpleSearchCycle(id obj, BOOL gatherAll); + +// Zero a set of references. This simply iterates the set and does +// CFRelease(*reference); *reference = NULL; for each item. void CircleZeroReferences(CFSetRef references); +// Info from the collector about an object. @interface CircleObjectInfo : NSObject +// The object pointer. Stored as a void * to allow greater control over retain/release @property void *object; + +// Whether the object is externally referenced. If NO, its retain count is exactly +// equal to what is caused by references within the cycle. If YES, some additional +// references are present. @property BOOL externallyReferenced; + +// Whether the object is part of the searched cycle, or just a leaf node. @property BOOL partOfCycle; + +// Whether the object has actualy been leaked. This is YES if all of the +// objects in the cycle have not been externally referenced. @property BOOL leaked; + +// The addresses of incoming references to this object that were found within the cycle. +// Conceptually the set stores id* values. @property CFMutableSetRef incomingReferences; + +// The addresses of referring objects within the cycle. @property CFMutableSetRef referringObjects; @end +// A very simple cycle finder. @interface CircleSimpleCycleFinder : NSObject +// Add a candidate object to the collector. This collector only searches starting from +// candidate objects it is given. It does not search arbitrary objects. - (void)addCandidate: (id)obj; + +// Run a collection cycle. This searches all candidates, then breaks any leaked cycles +// it finds by zeroing out the strong references found within a candidate. - (void)collect; + +// Run the info-gathering portion of a collection cycle. The return value is an array +// of array of CircleObjectInfo instances. Each sub-array corresponds to a candidate. +// If a candidate refers to another candidate, directly or indirectly, duplicate object +// infos may appear in different sub-arrays, because this collector is neither particularly +// smart nor efficient. - (NSArray *)objectInfos; @end diff --git a/Circle/CircleSimpleCycleFinder.m b/Circle/CircleSimpleCycleFinder.m index b98beb7..d6f26ab 100644 --- a/Circle/CircleSimpleCycleFinder.m +++ b/Circle/CircleSimpleCycleFinder.m @@ -34,6 +34,7 @@ - (void)dealloc CFRelease(_referringObjects); } +// Helper function used with CFSetApplyFunction. static void AddAddressString(const void *value, void *context) { NSMutableArray *array = (__bridge id)context; @@ -42,6 +43,7 @@ static void AddAddressString(const void *value, void *context) - (NSString *)description { + // Map the sets of references and referring objects into strings. NSMutableArray *incomingReferencesStrings = [NSMutableArray array]; CFSetApplyFunction(_incomingReferences, AddAddressString, (__bridge void *)incomingReferencesStrings); @@ -60,25 +62,40 @@ - (NSString *)description @end +// Scan all objects referenced by the given object, directly or indirectly, and generate +// CircleObjectInfo instances for them. Returns a dictionary mapping object addresses +// to info objects. static CFMutableDictionaryRef CopyInfosForReferents(id obj) { + // Info instances go into this dictionary. CFMutableDictionaryRef searchedObjs = CFDictionaryCreateMutable(NULL, 0, NULL, &kCFTypeDictionaryValueCallBacks); + + // An array of objects to search. This is used as a stack. CFMutableArrayRef toSearchObjs = CFArrayCreateMutable(NULL, 0, NULL); + // Start out searching the given object. CFArrayAppendValue(toSearchObjs, (__bridge void *)obj); + // Keep searching until the stack runs out of objects. CFIndex count; while((count = CFArrayGetCount(toSearchObjs)) > 0) { + // Pop the object to search from the stack. void **candidate = (void **)CFArrayGetValueAtIndex(toSearchObjs, count - 1); CFArrayRemoveValueAtIndex(toSearchObjs, count - 1); LOG(@"Scanning candidate %p, retain count %lu", candidate, CFGetRetainCount((CFTypeRef)candidate)); + // Go through all of the strong references in the object. EnumerateStrongReferences(candidate, ^(void **reference, void *target) { + // If the target is nil, there's nothing to do. if(target) { + // Get the object's info object. CircleObjectInfo *info = (__bridge CircleObjectInfo *)CFDictionaryGetValue(searchedObjs, target); + + // If there is no info object make one. Also add this target to the stack + // of objects to search, since we're the first one to get to it. if(!info) { info = [[CircleObjectInfo alloc] init]; @@ -87,6 +104,7 @@ static CFMutableDictionaryRef CopyInfosForReferents(id obj) CFArrayAppendValue(toSearchObjs, target); } + // Add the reference and object to the info object. CFSetAddValue([info incomingReferences], reference); CFSetAddValue([info referringObjects], candidate); } @@ -99,15 +117,18 @@ static CFMutableDictionaryRef CopyInfosForReferents(id obj) struct CircleSearchResults CircleSimpleSearchCycle(id obj, BOOL gatherAll) { + // Fetch object infos for everything referenced by object. CFMutableDictionaryRef infos = CopyInfosForReferents(obj); + // Fetch the object's info. CircleObjectInfo *info = (__bridge CircleObjectInfo *)CFDictionaryGetValue(infos, (__bridge void *)obj); if(!info) { + // If there's no info object for obj, then it's not part of any sort of cycle. If we + // aren't gathering all info, then we can just bail out now and return NO. if(!gatherAll) { - // short circuit: if there's no info object for obj, then it's not part of any sort of cycle struct CircleSearchResults results; results.isUnclaimedCycle = NO; results.referencesToZero = CFSetCreate(NULL, NULL, 0, NULL); @@ -116,7 +137,8 @@ struct CircleSearchResults CircleSimpleSearchCycle(id obj, BOOL gatherAll) } else { - // add an empty info for obj so the caller gets complete results + // The caller still wants information. Add an empty info for obj, since the caller + // is asking about it. info = [[CircleObjectInfo alloc] init]; [info setObject: (__bridge void *)obj]; CFDictionarySetValue(infos, (__bridge void *)obj, (__bridge void *)info); @@ -127,40 +149,63 @@ struct CircleSearchResults CircleSimpleSearchCycle(id obj, BOOL gatherAll) NSUInteger retainCount = CFGetRetainCount((__bridge CFTypeRef)obj); LOG(@"%@ retain count is %lu, scanned incoming references are %@", obj, retainCount, CFBridgingRelease(CFCopyDescription(incomingReferences))); + // Create a stack of objects to search. Start the search with obj. CFMutableArrayRef toSearchObjs = CFArrayCreateMutable(NULL, 0, NULL); CFArrayAppendValue(toSearchObjs, (__bridge void*)obj); + // Keep a list of objects we already searched, so we don't loop endlessly. CFMutableSetRef didSearchObjs = CFSetCreateMutable(NULL, 0, NULL); + // Track whether we found any externally retained objects. BOOL foundExternallyRetained = NO; + // Objects are part of a cycle if the object we're interested in has incoming references. + // (Cycles among objects it points to that don't involve the top object don't count.) BOOL hasCycle = CFSetGetCount(incomingReferences) > 0; + // Run through the object graph backwards, walking from each object to its referring objects. + // This eliminates leaf nodes. If any of these objects are externally referenced, then the + // cycle is not a leak. CFIndex count; while((count = CFArrayGetCount(toSearchObjs)) > 0) { + // Pop the stack, and add the popped object to the set of searched objects. void *cycleObj = (void *)CFArrayGetValueAtIndex(toSearchObjs, count - 1); CFArrayRemoveValueAtIndex(toSearchObjs, count - 1); CFSetAddValue(didSearchObjs, cycleObj); + // Fetch the object info for the object, its references, and how many references it has. CircleObjectInfo *info = (__bridge CircleObjectInfo *)CFDictionaryGetValue(infos, cycleObj); CFSetRef referencesCF = [info incomingReferences]; CFIndex referencesCount = CFSetGetCount(referencesCF); [info setPartOfCycle: hasCycle]; + // An object is externally referenced if its retain count is not equal to the number + // of strong references found within the cycle. CFIndex retainCount = CFGetRetainCount(cycleObj); + + // This is a naaaaaaasty hack. The object passed in to this function has its retain + // count bumped twice. Once because it's retrieved from a weak reference, and a second + // time because ARC retains function parameters. Compensate for that by subtracting 2 + // from its retain count. if(cycleObj == (__bridge void *)obj) retainCount -= 2; + // See if the retain count matches the number of references. if(retainCount != referencesCount) { foundExternallyRetained = YES; [info setExternallyReferenced: YES]; + + // If we weren't asked to gather all info, then we can just stop here. + // This isn't a leaked cycle, since a member of the cycle is externally + // referenced. if(!gatherAll) break; } + // Walk through all referring objects and add them to the stack of objects to examine. CFSetRef referringObjectsCF = [info referringObjects]; CFIndex referringObjectsCount = CFSetGetCount(referringObjectsCF); const void* referringObjects[referringObjectsCount]; @@ -169,6 +214,8 @@ struct CircleSearchResults CircleSimpleSearchCycle(id obj, BOOL gatherAll) for(unsigned i = 0; i < referringObjectsCount; i++) { const void *referrer = referringObjects[i]; + + // Make sure we don't search the same object twice. if(!CFSetContainsValue(didSearchObjs, referrer)) CFArrayAppendValue(toSearchObjs, referrer); } @@ -176,18 +223,25 @@ struct CircleSearchResults CircleSimpleSearchCycle(id obj, BOOL gatherAll) LOG(@"foundExternallyRetained is %d", foundExternallyRetained); + // Set all infos to leaked if no externally retained object was found. if(!foundExternallyRetained) { for(CircleObjectInfo *info in [(__bridge id)infos objectEnumerator]) [info setLeaked: YES]; } + // Create a set of references to zero to break the cycle. These are simply the strong + // references held by the object. (In theory, the references of any object in the cycle + // would work. In practice, we can't zero the references of all objects, e.g. arrays, + // and it's a bit dangerous to zero an arbitrary object's references since it might + // actually use them in dealloc or something. CFMutableSetRef referencesToZero = CFSetCreateMutable(NULL, 0, NULL); EnumerateStrongReferences((__bridge void *)obj, ^(void **reference, void *target) { if(target) CFSetAddValue(referencesToZero, reference); }); + // Create the results structure, release objects, and return. struct CircleSearchResults results; results.isUnclaimedCycle = !foundExternallyRetained; results.referencesToZero = referencesToZero; @@ -215,6 +269,7 @@ void CircleZeroReferences(CFSetRef references) } } +// A simple weak reference wrapper making it easy to fill an array with weak references. @interface _CircleWeakRef : NSObject @property (weak) id obj; @end @@ -236,11 +291,15 @@ - (id)init - (void)addCandidate: (id)obj { + // Candidates are stored as weak references so that the collector doesn't keep them + // alive in the instances where they don't leak, and so that the collector doesn't + // place additional retains on them which would confuse the cycle finder. _CircleWeakRef *ref = [[_CircleWeakRef alloc] init]; [ref setObj: obj]; [_weakRefs addObject: ref]; } +// Enumerate over all candidate objects, gathering cycle search results for each one. - (void)_enumerateObjectsGatherAll: (BOOL) gatherAll resultsCallback: (void (^)(struct CircleSearchResults results)) block { NSMutableIndexSet *zeroedIndices; @@ -248,12 +307,15 @@ - (void)_enumerateObjectsGatherAll: (BOOL) gatherAll resultsCallback: (void (^)( NSUInteger index = 0; for(_CircleWeakRef *ref in _weakRefs) { + // Fetch the real object in an autorelease pool to ensure that the only retain on + // it is the one from obj. id obj; @autoreleasepool { obj = [ref obj]; } if(obj) { + // Do the cycle search, call the block, and free the results. struct CircleSearchResults results = CircleSimpleSearchCycle(obj, gatherAll); block(results); CFRelease(results.referencesToZero); @@ -261,6 +323,8 @@ - (void)_enumerateObjectsGatherAll: (BOOL) gatherAll resultsCallback: (void (^)( } else { + // The weak reference is nil. Add it to the set of zeroed weak references + // for cleanup. if(!zeroedIndices) zeroedIndices = [NSMutableIndexSet indexSet]; [zeroedIndices addIndex: index]; @@ -268,10 +332,9 @@ - (void)_enumerateObjectsGatherAll: (BOOL) gatherAll resultsCallback: (void (^)( index++; } + // Remove all zeroed weak references from the array. if(zeroedIndices) [_weakRefs removeObjectsAtIndexes: zeroedIndices]; - - } - (void)collect