Wednesday, December 30, 2009

Invocation - Destroy thy-self!

I've been working over the past few days to tidy up some code I've done in one of our new iPhone games.
The tools that come with iPhone SDK for profiling and debugging are pretty good for a C based environment. However every now and again you come across a bug that reminds you this is a native language.

Although the retain/release concept is very simple it often produces confusing bugs relating to premature deallocation of objects. The reason being that the errors thrown up can easily be somewhere down the line as a result of a future memory access/alloc/free failing on the FREED object. Because these errors can occur elsewhere in the code (often seemingly at random locations) - these kind of problems can be *very* tricky to debug.


This particular issue was quite interesting:

One of my UI controls is a Button that accepts an onclick action from script. This onclick action is simply parsed as a selector that belongs to the current screen. It's used to script menus etc. simply and bind them to simple methods of the current screen.


-(void)setOnClick:(id) reciever Selector:(SEL) selector
{
[ clickInvocation release ];
clickInvocation = nil;

onClickReciever = reciever;
onClickSelector = selector;

if( onClickReciever != nil )
{
NSMethodSignature *sig = [ [ reciever class ]
instanceMethodSignatureForSelector:selector ];
clickInvocation = [ NSInvocation
invocationWithMethodSignature:sig ];
[ clickInvocation setTarget: onClickReciever ];
[ clickInvocation setSelector: onClickSelector ];

// If the selector takes an extra argument it's th
// event source.
if( [ sig numberOfArguments ] > 2 )
[ clickInvocation setArgument:&self atIndex:2];

[ clickInvocation retainArguments ];
[ clickInvocation retain ];
}
}

-(void)clicked
{
if( clickInvocation != nil )
{
[ clickInvocation invoke ];
}
}

You can see form the short code excerpt that when the onclick string is set, it simply creates an NSInvocation which the Button can invoke each time it gets clicked.

This has worked flawlessly until I started doing some profiling in Instruments yesterday.... I noticed that going between 2 menus in rapid succession, after a few times would crash... with seemingly random errors in malloc_error_break or EXC_BAD_ACCESS in many different parts of code.

Finding The Cause

I could see it was something to do with clearing the menus, and after exhaustively profiling the retain/release cycles of my XML parsing and UI code I started to think it must be threading. This was a no-go because all the touch events and drawing happen from the same NSRunLoop - and hence same thread.

Chasing the cause of the seemingly random errors in the debugger led me all over my XML parsing, drawing code and even to main(int argc, char *argv[]) - I was starting to wonder what was going on!

Finally I decided to try to reproduce the problem from a test harness without any touch input. Displaying the menus quickly in succession proved no problems, but as soon as I start to poke the Button's touch events to do it the problems re-occured.

Delving deeper; I started to poke the button's onclick internals until finally I realised what was going on...

At Last!

For Button's, often the selector set on onclick is one which shows a different menu. This means that selector is responsible for removing the current menu from the screen, which in turn will release all of the menus contents, releasing the Button which has just been pressed.

You'd assume that is not a problem, but obviously it is for the NSInvocation - when it gets deallocated before it has returned!

Solution

The fix is obviously to retain the NSInvocation before you call invoke and release afterward in the Buttons onclick method.
It is odd that you can deallocate an object that is part of the current call stack, but understandable I suppose on the grounds that the runtime doesn't want to incur the overhead of retaining each object on the stack.


In any case that's several hours of my life that I won't get back ;-) Now back to the original profiling task!

No comments: