[Objective-C + Cocoa] Object Inspection

I’m a big fan of reflection. Always have been since I was first exposed to it in Java. For those not familiar with the concept, reflection (or introspection as it is alternately called) allows one to inspect and/or access the properties and methods of an object instance at runtime, without needing any specific details about its declared type or fields. Though it may seem like a fairly minor feature, reflection is used to great effect in the Java world by the likes of Spring and EasyMock, to name just a few.

So it’s a bit puzzling to me, then, that reflection seems to have been long forgotten in the realm of Objective-C. Apple’s official documentation on this topic even mildly discourages its use (“You typically do not need to use the Objective-C runtime library directly when programming in Objective-C”). Granted, the performSelector: method sees fairly frequent use in many Cocoa applications, but this is a minor concession in a language where virtually every method-call resolves to a table lookup to find the implementation of the method being called (and you can even swap method implementations around at runtime by mucking with the lookup table).

So as a demonstration of some of the neat things that can be done using reflection in Objective-C, I’ve put together some code that will “deconstruct” an arbitrary object. This code will:

  • Print the signature of any methods that exist on an object.
  • Print the name and type of any properties declared on the object
  • Print the name and type of any instance-level fields declared as members of the object.
  • Optionally recurse through any non-primitive non-nil field/property types.
  • Optionally recurse through the object’s superclasses until the root class (NSObject, typically) is reached.
  • Attempt to track and return the real amount of memory allocated to the object (not fully accurate).

The code is packaged as a category on NSObject, meaning that if you include it in your project you can simply call ‘[obj printObject:obj toDepth:0]‘ in order to print the details of any object you are interested in. Anyways, here is the code that works all the magic:

#import <objc/objc-class.h>
#import <malloc/malloc.h>

@implementation NSObject(object_print)

- (NSString*) appendTo: (NSString*) base with: (NSString*) rest {
	return [NSString stringWithFormat:@"%@%@", base, rest];
}

- (int) printObjectInternal:(id)anObject printState: (NSMutableArray*)state friendlyName: (NSString*) objName withIndent: (NSString*)indent fromDepth: (int)currentDepth toDepth: (int)maxDepth {
	if (anObject == nil || anObject == NULL || currentDepth > maxDepth) {
		//nothing to do
		return 0;
	}
	
	[state addObject:anObject];
	
	//process properties for the class and its superclass(es)
	int totalSize = 0;
	int mySuperclassDepth = currentDepth;
	Class processingClass = [anObject class];
	while (processingClass != nil && processingClass != [NSObject class] && mySuperclassDepth <= maxDepth) {
		unsigned int numFields = 0;
		
		//methods
		Method* methods = class_copyMethodList(processingClass, &numFields);
		NSLog(@"[%@] - %@  Printing object:  type=%@ : %@ ...", objName, indent, processingClass, class_getSuperclass(processingClass));
		NSLog(@"[%@] - %@  Printing object methods:  type=%@, numMethods=%d", objName, indent, processingClass, numFields);
		for (int index = 0; index < numFields; index++) {
			unsigned int numArgs = method_getNumberOfArguments(methods[index]);
			const char* name = sel_getName(method_getName(methods[index]));
			NSString* argString = @"";
			char* copyReturnType = method_copyReturnType(methods[index]);
			for (int argIndex = 0; argIndex < numArgs; argIndex++) {
				char* argType = method_copyArgumentType(methods[index], argIndex);
				if (argIndex > 2) {
					argString = [argString stringByAppendingFormat:@" argName%d: (%@) arg%d", argIndex - 2, [self codeToReadableType: argType], argIndex - 2]; 
				}
				else if (argIndex > 1) {
					argString = [argString stringByAppendingFormat:@" (%@) arg%d", [self codeToReadableType: argType], argIndex - 2];
				}
				free(argType);
			}
			
			if (numArgs <= 2) {
				NSLog(@"[%@] - %@ (%@)  - (%@) %s;", objName, indent, processingClass, [self codeToReadableType: copyReturnType], name);
			}
			else {
				NSLog(@"[%@] - %@ (%@)  - (%@) %s %@;", objName, indent, processingClass, [self codeToReadableType: copyReturnType], name, argString);
			}
			free(copyReturnType);
		}
		
		//properties (i.e. things declared with '@property')
		objc_property_t* props = class_copyPropertyList(processingClass, &numFields);
		NSLog(@"[%@] - %@  Printing object properties:  type=%@, numFields=%d", objName, indent, processingClass, numFields);
		for (int index = 0; index < numFields; index++) {
			objc_property_t prop = props[index];
			const char* fieldName = property_getName(prop);
			const char* fieldType = property_getAttributes(prop);
			NSLog(@"[%@] - %@ (%@) @property %@ %s;", objName, indent, processingClass, [self codeToReadableType: fieldType], fieldName);
			
			@try {
				id fieldValue = [anObject valueForKey:[NSString stringWithFormat:@"%s", fieldName]];
				totalSize += malloc_size(fieldValue);
				NSString* typeString = [NSString stringWithFormat:@"%s", fieldType];
				NSRange range = [typeString rangeOfString:@"T@\""];
				if (range.location == 0 && fieldValue && ! [state containsObject:fieldValue]) {
					//the field is an object-type, so print its size as well
					NSLog(@"[%@] - %@ (%@)\t  Expanding property [%s]:", objName, indent, processingClass, fieldName);
					totalSize += [self printObjectInternal: fieldValue printState: state friendlyName: objName withIndent: [NSString stringWithFormat:@"%@\t", indent] fromDepth: mySuperclassDepth + 1 toDepth: maxDepth];
				}
			}
			@catch (id ignored) {
				//couldn't get it with objectForKey, so try an alternate way
				void* fieldValue = NULL;
				object_getInstanceVariable(anObject, fieldName, &fieldValue);
				if (fieldValue != NULL && fieldValue != nil) {
					totalSize += malloc_size(fieldValue);
				}
			}
		}
		
		//ivars (i.e. declared instance members)
		Ivar* ivars = class_copyIvarList(processingClass, &numFields);
		NSLog(@"[%@] - %@ (%@) Printing object ivars:  type=%@, numFields=%d", objName, indent, processingClass, processingClass, numFields);
		for (int index = 0; index < numFields; index++) {
			Ivar ivar = ivars[index];
			id fieldValue = object_getIvar(anObject, ivar);
			
			const char* fieldName = ivar_getName(ivar);
			const char* fieldType = ivar_getTypeEncoding(ivar);
			
			NSLog(@"[%@] - %@ (%@) %@ %s;", objName, indent, processingClass, [self codeToReadableType: fieldType], fieldName);
			int mSize = malloc_size(fieldValue);
			totalSize += mSize;
			
			@try {
				NSString* typeString = [NSString stringWithFormat:@"%s", fieldType];
				NSRange range = [typeString rangeOfString:@"@"];
				if (range.location == 0 && (! [state containsObject:fieldValue]) && mSize > 0) {
					//the field is an object-type, so print its size as well
					NSLog(@"[%@] - %@ (%@)\t  Expanding ivar [%s]:", objName, indent, processingClass, fieldName);
					totalSize += [self printObjectInternal: fieldValue printState: state friendlyName: objName withIndent: [NSString stringWithFormat:@"%@\t", indent] fromDepth: mySuperclassDepth + 1 toDepth: maxDepth];
					
					//see if it's a countable type, just for fun
					if ([fieldValue respondsToSelector:@selector(count)]) {
						//if we can count it, print the count
						NSLog(@"[%@] - %@ (%@)\t\t  Container Count:  name=%s, type=%s, count=%d", objName, indent, processingClass, fieldName, fieldType, [fieldValue count]);
					}
				}
			}
			@catch (id ignored) {
				//couldn't print it
			}
		}
		
		//process indexed ivars (extra bytes allocated at end of object; no name available, just size)
		void* extraBytes = object_getIndexedIvars(anObject);
		NSLog(@"[%@] - %@ (%@) Printing object indexedIvars:  type=%@, extraBytes=%d", objName, indent, processingClass, processingClass, malloc_size(extraBytes));
		
		//process superclass
		NSLog(@"[%@] - %@ (%@) Superclass of %@ is %@", objName, indent, processingClass, processingClass, class_getSuperclass(processingClass));
		processingClass = class_getSuperclass(processingClass);
		mySuperclassDepth++;
	}
	
	return totalSize;
}

- (int) printObject: (id)anObject toDepth: (int) maxDepth {
	if (! anObject) {
		anObject = self;
	}
	if (maxDepth < 0) {
		maxDepth = 0;
	}
	NSMutableArray* state = [[NSMutableArray alloc] initWithCapacity: 1024];
	int result = [self printObjectInternal:anObject printState: state friendlyName: [[anObject class] description] withIndent: @"" fromDepth: 0 toDepth: maxDepth];
	[state release];
	
	return result;
}

@end

One minor omission from the above code is the codeToReadableType: function. This method simply takes an Objective-C type code (things like “^^f” and “C” and “:”) and parses it back into a human-readable format. It’s rather verbose for what it does, so it’s available at the end of this post.

Anyways, this code imbues any type derived from NSObject with a ‘printObject:toDepth:‘ method which does pretty much what its name implies. For a given object, it will print information about its methods, properties, and fields, and if you specify a depth greater than 0 it will also recurse through any non-primitive property or field and also the object’s superclass(es) to the specified depth limit. Want to know all 327 methods that exist on a UIView instance, including the ones that Apple doesn’t tell you about? Then invoke this method on a UIView instance, or on an instance of anything that extends UIView using a ‘toDepth:‘ of 1 or greater.

Also note that this method is designed to handle circular references in the object hierarchy and avoid getting trapped in a cycle; if you paid close attention to the code you will have noticed the ‘state‘ array, which keeps track of every object instance that has been encountered in the hierarchy. Before recursing through a new object instance the method first checks this array to make sure that instance hasn’t already been encountered, and avoids recursing through the object if it has already been seen. So you don’t have to worry about circular references killing the ‘printObject:toDepth:‘ routine.

All-told, this code can be quite fun to play with if you’re curious about your Objective-C runtime environment. It lets you see in a human-readable way what the object hierarchy looks like in memory. Note that while this code also attempts to keep track of the number of bytes allocated by each object it traverses, it does not do a complete job of it, and the returned value shouldn’t be assumed to be an accurate representation of the size of the object instance. It may work for simple types, but you certainly shouldn’t rely on it for anything significant.

Lastly, here is the codeToReadableType: implementation. It could almost certainly be more compactly written using regular expressions and pattern matching, but this will get the job done. Just include it as part of the category, and you’ll be all set.

- (NSString*) codeToReadableType: (const char*) code {
	NSString* codeString = [NSString stringWithFormat:@"%s", code];
	NSString* result = [NSString stringWithString:@""];
	
	bool array = NO;
	NSString* arrayString;
	//note:  we parse our type from left to right, but build our result string from right to left
	for (int index = 0; index < [codeString length]; index++) {
		char nextChar = [codeString characterAtIndex:index];
		switch (nextChar) {
			case 'T':
				//a placeholder code, the actual type will be specified by the next character
				break;
			case ',':
				//used in conjunction with 'T', indicates the end of the data that we care about 
				//we could further process the character(s) after the comma to work out things like 'nonatomic', 'retain', etc., but let's not
				index = [codeString length];
				break;
			case 'i':
				//int or id
				if (index + 1 < [codeString length] && [codeString characterAtIndex:index + 1] == 'd') {
					//id
					result = [self appendTo: (array ? @"id[" : @"id") with: result];
					index++;
				}
				else {
					//int
					result = [self appendTo: (array ? @"int[" : @"int") with: result];
				}
				break;
			case 'I':
				//unsigned int
				result = [self appendTo: (array ? @"unsigned int[" : @"unsigned int") with: result];
				break;
			case 's':
				//short
				result = [self appendTo: (array ? @"short[" : @"short") with: result];
				break;
			case 'S':
				//unsigned short
				result = [self appendTo: (array ? @"unsigned short[" : @"unsigned short") with: result];
				break;
			case 'l':
				//long
				result = [self appendTo: (array ? @"long[" : @"long") with: result];
				break;
			case 'L':
				//unsigned long
				result = [self appendTo: (array ? @"unsigned long[" : @"unsigned long") with: result];
				break;
			case 'q':
				//long long
				result = [self appendTo: (array ? @"long long[" : @"long long") with: result];
				break;
			case 'Q':
				//unsigned long long
				result = [self appendTo: (array ? @"unsigned long long[" : @"unsigned long long") with: result];
				break;
			case 'f':
				//float
				result = [self appendTo: (array ? @"float[" : @"float") with: result];
				break;
			case 'd':
				//double
				result = [self appendTo: (array ? @"double[" : @"double") with: result];
				break;
			case 'B':
				//bool
				result = [self appendTo: (array ? @"bool[" : @"bool") with: result];
				break;
			case 'b':
				//char and BOOL; is stored as "bool", so need to ignore the next 3 chars
				result = [self appendTo: (array ? @"BOOL[" : @"BOOL") with: result];
				index += 3;
				break;
			case 'c':
				//char?
				result = [self appendTo: (array ? @"char[" : @"char") with: result];
				break;
			case 'C':
				//unsigned char
				result = [self appendTo: (array ? @"unsigned char[" : @"unsigned char") with: result];
				break;
			case 'v':
				//void
				result = [self appendTo: @"void" with: result];
				break;
			case ':':
				//selector
				result = [self appendTo: @"SEL" with: result];
				break;
			case '^':
				//pointer
				result = [self appendTo: @"*" with: result];
				break;
			case '@': {
				//object instance, may or may not include the type in quotes, like @"NSString"
				if (index + 1 < [codeString length] && [codeString characterAtIndex:index + 1] == '"') {
					//we can get the exact type
					int endIndex = index + 2;
					NSString* theType = @"";
					while ([codeString characterAtIndex:endIndex] != '"') {
						theType = [NSString stringWithFormat:@"%@%c", theType, [codeString characterAtIndex:endIndex]];
						endIndex++;
					}
					theType = [self appendTo: theType with: @"*"];
					result = [self appendTo: theType with: result];
				
					index = endIndex + 1;
				}
				else {
					//all we know is that it's an object of some kind
					result = [self appendTo: @"NSObject*" with: result];
				}
				break;
			}
			case '{': {
				//struct, we don't fully process these; just echo them
				index++;
				int numBraces = 1;
				NSString* theType = @"{";
				while (numBraces > 0) {
					char next = [codeString characterAtIndex:index];
					theType = [NSString stringWithFormat:@"%@%c", theType, next];
					if (next == '{') {
						numBraces++;
					}
					else if (next == '}') {
						numBraces--;
					}
					
					index++;
				}
				result = [NSString stringWithFormat:@"struct %@%@", theType, result];
				
				index--;
				break;
			}
			case '?':
				//IMP and function pointer
				result = [self appendTo: @"IMP" with: result];
				break;
			case '[':
				//array type
				array = YES;
				arrayString = @"";
				result = [self appendTo: @"]" with: result];
				break;
			case ']':
				//array type
				array = NO;
				break;
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
				//for a statically-sized array, indicates the number of elements
				if (array) {
					arrayString = [NSString stringWithFormat:@"%@%c", arrayString, nextChar];
				}
				break;
			default:
				break;
		}
	}
	
	return result;
}
This entry was posted in coding, objective-c and tagged , . Bookmark the permalink.

3 Responses to [Objective-C + Cocoa] Object Inspection

  1. Pingback: nuclear power stations

  2. Thanks for that awesome posting. It saved MUCH time :-)

  3. comm1 says:

    I really enjoyed your insight on this. Pretty unrelated, but it reminds me of when I was dealing with altadonna communications. He really guided me through an extremely important time.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>