April 28, 2009

Invoking Selectors

There are many instances in the Cocoa-Touch SDK where you need to specify a callback method. For example, UIButton has a method where you set the callback for when the button is hit. Or an NSTimer can have a callback for when the timer expires. UIBarButtonItem also has a callback when the button is hit. Every one of these examples follows the same pattern: you have to provide the target (usually self, but doesn't have to be), and a selector created with the mysterious @selector() construct. Wouldn't it be neat if you could use these target/selector pairs in your own code? There are many possibilities of why you would want to implement your own callbacks, like writing your own GUI buttons, or you wanted to create a convenience class to make threading a little easier. Let's demonstrate using that last example: create a convenient Thread wrapper class. This class is very similar to the NSOperation class, so this is really for demonstration purposes and not something terribly useful.
The NSThread class requires you to provide the target/selector for the function's main entry point. That method must create a new autorelease pool, and then release it when it is done. It would be nice if we didn't have to do that boilerplate code every time we wanted to spawn off a thread. So a wrapper class can do it for us. Let's call this new class MyThread:
// MyThread.h

@interface MyThread
{
NSThread* thread;
NSInvocation* inv;
}

@property(nonatomic,retain) NSThread* thread;
@property(nonatomic,retain) NSInvocation* inv;

- (id) initWithTarget:(id)target selector:(SEL) selector object:(id) argument;
- (void) start;

@end

Obviously, this class doesn't do as much as the NSThread class, but these two methods are sufficient to demonstrate method invocation. What this class does is create the NSThread object, then provide the thread main entry point, which points back into the MyThread class. This will create the autorelease pool, then call the "other" entry point (the one that was supplied to MyThread). When that returns, it releases the pool. The key here, and the point of this article, is calling the other entry point. It is not trivial, as you'll soon see. Most of the grunt work is done in the MyThread initializer:

- (id) initWithTarget:(id)target selector:(SEL) selector object:(id) argument
{
  // create the thread object
  thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];

  // create the invocation object
  //
  // first we need the method signature
  NSMethodSignature* sig = [target methodSignatureForSelector:selector];

  // now create the invocation object from the signature
  NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig];

  // setup the invocation
  [inv setTarget:target];
  [inv setSelector:selector];

  // set the argument, if there is one
  if(argument) {
      [inv setArgument:&argument atIndex:2];
  }

  [inv retainArguments];
}

If you are familiar with Java, you'll notice that the NSInvocation class is similar to the java Method class in the reflection package. The NSInvocation object is what you use to piece together a method call at runtime. Once it's all put together, you can invoke it. This code first constructs a NSMethodSignature object which is used to create the NSInvocation object. Then we configure the target and selectors (not sure why, since the signature should have all that embedded). Finally, we set the arguments to the call. The argument goes at index 2 because the self argument occupies index 0, and the _cmd argument takes index 1. Thus, any additional arguments start at index 2. Now everything is all setup in our MyThread class. All we do is wait for the thread to be started via the start method. This just delegates the start to the internal NSThread object:
- (void) start
{
   [thread start];
}

The thread is configured to use the thread entry point threadMain defined in MyThread. This is where we put the boilerplate pool code, then call the callback supplied to us in the initializer.
- (void) threadMain
{
    // create the autorelease pool
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // now run the selector provided to us
    [inv invoke];

    // release the pool
    [pool release];
}

The NSInvocation class is a little clumsy to set up, but it can be quite powerful when you want to specify and use your own callbacks.

No comments:

Post a Comment