Home > Cocoa Libraries, Optimization, Threading > Part II: Spawning Threads Using Selectors With Multiple Parameters

Part II: Spawning Threads Using Selectors With Multiple Parameters

In Part I, we introduced a method that emulates the Apple-supplied convenience method performSelectorInBackground:withObject:, but which adds support for selectors that take multiple objects as arguments. We saw the power of NSInvocation objects, but also saw how they were a bit cumbersome to use on a routine basis. Our solution gave us what we wanted by allowing us to pass in an array of objects to be used, along with a multi-parameter selector, to invoke an arbitrary method on a background thread (or on the main thread).

This solution is a great step forward when we need to invoke something like [myObj setCharacter:@"The Doctor" currentActor:@"Matt Smith"]. But what if we have a bunch of methods that don’t take objects? Wrapping them all in NSValues begins to really litter-up the code, and is frankly a bit tedious. And what if one want to invoke something like [myObj setCharacter:@"The Doctor" favoriteActors:@"Tom Baker", @"David Tennant", @"Matt Smith"]? There has to be a cleaner way, right? Behold! There is, and the answer lies in the somewhat obscure-ish va_arg macro. Now we can save the world from our unwieldy code. Allons-y!

We are already familiar with methods that take a nil-terminated list of arguments. For example, NSArray offers its arrayWithObjects: method. We know that we can pass a list of comma-separated objects to this method, so long as the list ends with a nil.

Hmm…wouldn’t it be handy if we could do the same thing with our own method? Enter the va_list.

Let’s begin with our method’s definition in our header file. We need some way of letting the compiler know that we don’t know just how many arguments arguments to expect ahead of time. Apple’s documentation for NSArray use the ellipsis, that is three periods in a row without spaces. That’s not just a mere convention that prose writers use. It’s actually a symbol that the compiler understands to mean a nil-terminated list. Molto bene! Now our method definition looks something like:

- (void)performSelector:(SEL)aSelector withContext:(void *)context, ... ;

To invoke a method on another thread, we went from:

  • [myObj performSelector:@selector(setActorCount:role:) withContext:[NSArray arrayWithObjects: [NSValue valueWithBytes:&doctorCount objCType:@encode(int)], @"The Doctor", nil]] (yikes!)

to:

  • [myObj performSelector:@selector(setActorCount:role:) withContext:(int *)doctorCount, @"The Doctor", nil]

That’s more like it! Notice how our simplified method signature is also much easier to deal with when debugging? That alone makes the change worth while. So now that we’ve settled that, let’s take a look at our revised method’s implementation:

- (void)performSelector:(SEL)aSelector
			 withValues:(void *)context, ...
{	// See if we were even passed any context.
	if (context)
	{
		// We'll need an NSInvocation, since it lets us define arguments
		// for multiple parameters.
		NSMethodSignature *theSignature = [self methodSignatureForSelector:aSelector];
		NSInvocation *theInvocation = [NSInvocation invocationWithMethodSignature:theSignature];
		[theInvocation retainArguments];

		// Now for the real automagic fun!  We will loop through our arugment list,
		// and match up each parameter with the corresponding index value.

		// Since Objective-C actually converts messages into C methods:
		NSUInteger argumentCount = [theSignature numberOfArguments] - 2;

		[theInvocation setTarget:self];			// There's our index 0.
		[theInvocation setSelector:aSelector];	// There's our index 1.

		// Use the va_* macros to retrieve the arguments.
		va_list arguments;

		// Tell it where the optional args start.
		// Since the first parameter, the selector
		// Isn't optional, tell it to start with 'context'.
		va_start (arguments, context);

		// Now for arguments 2+
		NSUInteger i, count = argumentCount;
		void* currentValue = context;
		for (i = 0; i < count; i++)
		{
			// If we run out of arguments, then we pass nil
			// to the remaining parameters.
			[theInvocation setArgument:&currentValue atIndex:(i + 2)]; // The +2 represents self and cmd offsets
			currentValue = va_arg(arguments, void*); // Read the next argument in the list.

			// We should also handle the case where we have
			// *more* arguments than parameters.  This will
			// let us cover cases where we are invoking
			// other variadic methods (like arrayWithObjects:).
		}

		// Dispose of our C byte-array.
		va_end (arguments);

		// That's it! (For configuring our NSInvocation).
		// In this example, we are going to invoke all
		// methods on a custom worker thread.
		NSThread *customWorkerThread = [EBCustomThread sharedCustomThread];

		// Invoke on our custom worker thread.
		[theInvocation performSelector:@selector(invoke) onThread:customWorkerThread withObject:nil waitUntilDone:NO];

		// Our NSInvocation is already autoreleased, so we're done.

	} else {
		// Since we were not given any context arguments,
		// just do a non-blocking invocation on a background thread.
		// We really would want to check to see that the selector
		// is valid, takes no arguments, etc., but that is left
		// as an exercise for the reader.
		[self performSelectorInBackground:aSelector withObject:nil];
	} 
}

So there you have it.

[Update: Doug, the lead over at the the Zoosk.com mobile dev, has taken the time to add the above method as a category on NSObject as part of the their very useful ZSFoundation library. Big thanks to Doug and his team for sharing!]

If you are interested in more threading tricks, check out these other fine articles:

Happy threading!

Advertisements
  1. 05/18/2010 at 12:50 PM

    Bonus points for the Doctor Who reference!

    • Zack
      05/18/2010 at 12:52 PM

      Bonus points for catching them! : )

  2. Dennis
    10/27/2010 at 1:03 PM

    Great article, grazie mille!

    • Zack
      10/27/2010 at 6:31 PM

      Prego! Glad it was helpful. : )

  3. 02/15/2011 at 6:35 PM

    Thank you! If there was a super like button here.. I would have clicked it 1000 times.

    • Zack
      02/15/2011 at 6:43 PM

      : ) Glad you found it helpful!

  4. 03/03/2011 at 9:01 AM

    This looks really great. I’ve been trying to implement this, but… could you give a concrete example of how to implement:

    NSThread *customWorkerThread = [EBCustomThread sharedCustomThread];

    I tried this:

    // NSThread *customWorkerThread = [EBCustomThread sharedCustomThread];
    NSThread *customWorkerThread = [[[NSThread alloc] init] retain];

    Is this even close?

    Hmm, so maybe I’m approaching this wrong. Seems like this should be made a category(?) of NSObject if I want to make this generally available.

    Thoughts?

    • Zack
      03/03/2011 at 11:55 AM

      In my example, EBCustomThread was just a subclass of NSThread, which exposes itself as a singleton by sending it the sharedCustomThread message. You just need to make sure that the thread is running, or is started after you send it some work. EBCustomThread, in my case, stays running as it’s bound to a network port.

      And I love your suggestion of adding it was a category on NSObject! 🙂

  1. 05/18/2010 at 12:39 PM

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: