CocoaDev

Edit AllPages

I’m working on a framework with 25-30 classes, and right now I’m in a testing phase, writing nice test cases for most of the classes. While doing this I discover lots of memory leaks, and fix them. However, I only discover the leaks as a result of testing the other code, so I’m probably missing some. In one of the cases, I wrote a simple addition to one of my classes, so that it incremented a counter whenever it was alloc’ed and decremented the same counter when it was dealloc’ed, effectively giving me a counter telling me how many instances of this class that were around at any given time. Then I calculated the difference between the number of instances before the tested code, and compared that to the number after (making sure that I had allocated a new autorelease pool for that code, and released it before the comparation). This was very handy for detecting where the memory leaks actually occured.

Now, I want to do this with any class. I know that there are two utilities called OmniObjectMeter and ObjectAlloc, that can do something like this. But what I want to do is to include this check in my testcases, which means that running ObjectAlloc or whatever is not an option. What I want to do is call a function that gives me the current instance count of a named class (any class, not just my own), then execute some code and compare the new result from that function with the old in an assertion or something. Then I can select the test target in my project, type command-r and just wait for the message “All tests passed”.

So, how do I do this? Apple and Omni knows how, so it must be possible.

–TheoHultberg/Iconara


You could try using MethodSwizzling to add to NSObject’s allocWithZone:/retain/release methods. – KritTer


Nice idea, I will try this out and report back. – TheoHultberg/Iconara


OK, here’s my implementation: (link removed, see below instead)

Example

// wrap the counting in an autorelease pool, so that we can // release any autoreleased objects before counting NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

int count;

IXStartInstanceCounting();

count = IXInstaceCountForClass( [Foo class] );

doSomethingThatMightLeakMemory();

// important! if the pool is not released, the instance count // will be higher because of any autoreleased objects in the pool [pool release];

// check so that the number of instances of Foo is the same as // before the function call that might leak (in this case zero) NSCAssert( diff == IXInstaceCountForClass( [Foo class] ), @”Leaking memory” );

IXEndInstanceCounting();

You can also use the function IXAllInstanceCounts() to get a dictionary with all the classes that have been allocated since the count started, and the number of instances of them.

I have decided on having a start and end function since the code that does the counting shouldn’t be on always (it’s a lot of uneccesary instructions if you’re not interested in the counting).

– TheoHultberg/Iconara


Can you rewrite your code to automatically report *all memory leaks? Having to do it per class could well miss mistakes, e.g. where you used a temporary NSString and forgot to release it properly. – KritTer*


Yeah, sure, I’ll do that. There are problems with it though, NSString isn’t just NSString, it’s reported as NSCFString and other kinds as well (because of NSString being a ClassCluster), but what the hell, it’s no big deal. –TheoHultberg/Iconara


OK, now, if you call the function IXEndInstanceCountingAndCheckLeaks() (hey, that was a long name) it’ll trow an exception (IXMemoryLeakException) warning you that there are alloc’ed objects that have not been dealloc’ed. The message will tell you which classes and how many instances there are left. The userInfo dictionary contains all classes and their counts. – TheoHultberg/Iconara (who loves when he can solve his own problems with just a nudge in the right direction from someone, cheers KritTer)

No problemo. In the interest of global harmony…or…something…anyway, I’m posting the code you made up here. We can then scour it and refactor it, and it’s up even if your server perishes :) Take it down of course if you object. – KritTer

Good idea –TheoHultberg


IXInstanceCounting.h

/*

#import <Foundation/Foundation.h>

/*!

/*!

/*!

/*!

/*!

/*!

/*!

/*!

/*!

/*!

/*!

@end


IXInstanceCounting.m

#import “/usr/include/objc/objc-class.h”

#import “IXInstanceCounting.h”

void _IXICSwizzle(void); void _IXMethodSwizzle(Class aClass, SEL orig_sel, SEL alt_sel);

NSString *IXMemoryLeakException = @”IXMemoryLeakException”; NSString *IXInvalidStateException = @”IXInvalidStateException”;

static NSMutableDictionary *IXInstanceCounts = nil;

@interface NSObject ( InstanceCounting )

@end

void IXStartInstanceCounting( ) { if ( IXInstanceCounts == nil ) { [IXInstanceCounts release]; IXInstanceCounts = [[NSMutableDictionary alloc] init];

	_IXICSwizzle();
} else {
	[NSException raise:IXInvalidStateException
	            format:@"Instance counting already in progress"];
} }

void IXEndInstanceCounting( ) { if ( IXInstanceCounts != nil ) { _IXICSwizzle();

	[IXInstanceCounts release];

	IXInstanceCounts = nil;
} }

int IXInstanceCountForClass( Class class ) { if ( IXInstanceCounts != nil ) { IXCount *i = [IXInstanceCounts objectForKey:class];

	return [i value];
} else {
	[NSException raise:IXInvalidStateException
	            format:@"Instance counting not in progress"];
	return nil;
} }

NSDictionary *IXAllInstanceCounts( ) { if ( IXInstanceCounts != nil ) { return IXInstanceCounts; } else { [NSException raise:IXInvalidStateException format:@”Instance counting not in progress”]; return nil; } }

void IXEndInstanceCountingAndCheckLeaks( ) { if ( IXInstanceCounts != nil ) { NSException *exception; NSMutableDictionary *leaks = [NSMutableDictionary dictionary]; NSMutableString *leakedClasses = [NSMutableString string]; NSEnumerator *e; Class c;

	_IXICSwizzle();

	e = [IXInstanceCounts keyEnumerator];
	
	if ( c = [e nextObject] ) {
		IXCount *count = [IXInstanceCounts objectForKey:c];
		
		if ( [count value] > 0 ) {
			[leakedClasses appendFormat:@"%@ (%d)", NSStringFromClass(c), [count value]];
		}
	}
	
	while ( c = [e nextObject] ) {
		IXCount *count = [IXInstanceCounts objectForKey:c];
	
		if ( [count value] > 0 ) {
			[leakedClasses appendFormat:@", %@ (%d)", NSStringFromClass(c), [count value]];
			[leaks setObject:count forKey:c];
		}
	}
	
	if ( [leaks count] > 0 ) {
		NSString *reason = [NSString stringWithFormat:@"Memory leak detected: %@", leakedClasses];
	
		exception = [NSException exceptionWithName:IXMemoryLeakException 
		                                    reason:reason
		                                  userInfo:IXInstanceCounts];
	} else {
		exception = nil;
	}

	[IXInstanceCounts release];

	IXInstanceCounts = nil;
	
	[exception raise];
} else {
	[NSException raise:IXInvalidStateException
	            format:@"Instance counting not in progress"];
}  }

void _IXICSwizzle( ) { _IXMethodSwizzle([NSObject class], @selector(init), @selector(ixic_init)); _IXMethodSwizzle([NSObject class], @selector(dealloc), @selector(ixic_dealloc)); }

void _IXMethodSwizzle(Class aClass, SEL orig_sel, SEL alt_sel) { /* * IXMethodSwizzle * This function exchanges one implementation of * a method with another, it is used by IXStartInstanceCounting * and IXEndInstanceCounting to enable or disable the instance * counting scheme. * Kudos to Jack Nutting & Kritter for this code. See * http://www.cocoadev.com/index.pl?MethodSwizzling * for more information. */

Method orig_method = nil, alt_method = nil;

// First, look for the methods
orig_method = class_getInstanceMethod(aClass, orig_sel);
alt_method = class_getInstanceMethod(aClass, alt_sel);

// If both are found, swizzle them
if ((orig_method != nil) && (alt_method != nil)) {
	char *temp1;
	IMP temp2;

	temp1 = orig_method->method_types;
	orig_method->method_types = alt_method->method_types;
	alt_method->method_types = temp1;

	temp2 = orig_method->method_imp;
	orig_method->method_imp = alt_method->method_imp;
	alt_method->method_imp = temp2;
} }

@implementation IXCount

@end

@implementation NSObject ( InstanceCounting )

@end


Changes: