Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

A ZoomingViewController to animate a UIView to fullscreen

ZoomingViewController is a class you can attach to any existing view that will let you zoom the view to fullscreen with a single tap, rotate the view while in fullscreen by rotating the device and tap to return to the original inline state.

Introduction

The ZoomingViewController class in this week's sample project handles the zooming in the following sample app:

zoomingviewcontroller.png

The animation between the inline and fullscreen states is smooth and you can rotate the device in fullscreen to rotate the view.

You can attach the ZoomingViewController to any view to add this behavior.

You can download the ZoomingViewController and the complete sample project used in this post here TapZoomRotate.zip (160kb)

Requirements

The ZoomingViewController has three primary requirements:

  • Respond to tap actions
  • Smoothly zoom between different superviews
  • Allow the view to rotate in fullscreen

Responding to tap actions

Most of the time, you don't need to handle tap actions yourself — controls like buttons or table view cells all have built-in tap detection.

Prior to iOS 3.2, detecting a tap meant implementing touchesBegan:withEvent: and touchesEnded:withEvent: in the view or responder chain. This was fiddly since you had to perform time and location based calculations yourself to determine if the tap completed validly. Further, it would not have worked for this ZoomingViewController class, which only attaches to the view rather than being part of the the view or responder chain implementation.

Fortunately, the gesture recognizers introduced in iOS 3.2 make this trivially easy:

singleTapGestureRecognizer =
    [[UITapGestureRecognizer alloc]
        initWithTarget:self action:@selector(toggleZoom:)];
singleTapGestureRecognizer.numberOfTapsRequired = 1;

[self.view addGestureRecognizer:singleTapGestureRecognizer];

Smoothly zooming between different superviews

First, we need to remember the old superview and location within it. We do this by inserting a proxy view into the hierarchy at the view's location — this way, it will also track the autoresized location of the view if the window rotates.

proxyView = [[UIView alloc] initWithFrame:self.view.frame];
proxyView.hidden = YES;
proxyView.autoresizingMask = self.view.autoresizingMask;
[self.view.superview addSubview:proxyView];

Then, we need to calculate our current position in the coordinate space of the target view (the target view is the window itself for animating to fullscreen). With this location calculated, we switch superview to the target view and set the frame to this calculated position.

CGRect frame =
    [self.view.window
        convertRect:self.view.frame
        fromView:proxyView.superview];
[self.view.window addSubview:self.view];
self.view.frame = frame;

Then, we animate from this calculated position to actually fill the screen:

[UIView
    animateWithDuration:0.2
    animations:^{
        self.view.frame = self.view.window.bounds;
    }];
[[UIApplication sharedApplication]
    setStatusBarHidden:YES
    withAnimation:UIStatusBarAnimationFade];

Allowing the view to rotate in fullscreen

ZoomingViewController is not actually a subclass of UIViewController. Even though it "controls" a view for the purpose of zooming in and out, it doesn't require any of the behavior from UIViewController to do this.

Even if you did make the class a UIViewController subclass, it still wouldn't let you use the auto rotate behavior built into UIViewController. The reason is that shouldAutorotateToInterfaceOrientation: is only invoked on the first UIViewController found in the window. Since the fullscreen view will always be the second view in the window (after the inline view from which it zoomed), the UIViewController auto rotate behavior won't be able to satisfy our rotation needs.

So we need to implement rotation ourselves. This requires 3 steps:

  • Determine the correct fullscreen bounds for a given orientation
  • Calculate a CGAffineTransform to transform from the current bounds to the new bounds after a rotation
  • Listen to UIDeviceOrientationDidChangeNotification and actually apply these new values when things change

Getting the bounds for the fullscreen view based on the orientation is pretty simple too. We do need to account for the strangeness of face up and face down orientations — I do this by taking the status bar orientation in these cases instead (I don't use the status bar all the time in case it is out of sync with the actual device for some reason).

- (CGRect)rotatedWindowBounds
{
    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
    if (orientation == UIDeviceOrientationFaceUp ||
        orientation == UIDeviceOrientationFaceDown)
    {
        orientation = [UIApplication sharedApplication].statusBarOrientation;
    }
    
    if (orientation == UIDeviceOrientationLandscapeLeft ||
        orientation == UIDeviceOrientationLandscapeRight)
    {
        CGRect windowBounds = self.view.window.bounds;
        return CGRectMake(0, 0, windowBounds.size.height, windowBounds.size.width);
    }

    return self.view.window.bounds;
}

After a UIDeviceOrientationDidChangeNotification, we apply these new bounds.

Unfortunately, applying different bounds has the effect of changing the coordinates of view's midpoint. Since the final step of rotating the view requires applying a rotation transformation and rotations are always performed around the midpoint of the view, we must also translate the view in situations where the bounds have changed so that the midpoint is the same as it was previously.

- (CGAffineTransform)orientationTransformFromSourceBounds:(CGRect)sourceBounds
{
    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
    if (orientation == UIDeviceOrientationFaceUp ||
        orientation == UIDeviceOrientationFaceDown)
    {
        orientation = [UIApplication sharedApplication].statusBarOrientation;
    }
    
    if (orientation == UIDeviceOrientationPortraitUpsideDown)
    {
        return CGAffineTransformMakeRotation(M_PI);
    }
    else if (orientation == UIDeviceOrientationLandscapeLeft)
    {
        CGRect windowBounds = self.view.window.bounds;
        CGAffineTransform result = CGAffineTransformMakeRotation(0.5 * M_PI);
        result = CGAffineTransformTranslate(result,
            0.5 * (windowBounds.size.height - sourceBounds.size.width),
            0.5 * (windowBounds.size.height - sourceBounds.size.width));
        return result;
    }
    else if (orientation == UIDeviceOrientationLandscapeRight)
    {
        CGRect windowBounds = self.view.window.bounds;
        CGAffineTransform result = CGAffineTransformMakeRotation(-0.5 * M_PI);
        result = CGAffineTransformTranslate(result,
            0.5 * (windowBounds.size.width - sourceBounds.size.height),
            0.5 * (windowBounds.size.width - sourceBounds.size.height));
        return result;
    }

    return CGAffineTransformIdentity;
}

Finally, we apply alls these values in response to the UIDeviceOrientationDidChangeNotification. Listening to the notification is easy, we just add ourselves as an observer when switching into fullscreen display:

[[NSNotificationCenter defaultCenter]
    addObserver:self
    selector:@selector(deviceRotated:)
    name:UIDeviceOrientationDidChangeNotification
    object:[UIDevice currentDevice]];

The only additional point to note is that when a view rotates, it exposes the view behind its corners briefly. To avoid this potentially unappealing situation, the ZoomingViewController uses a blanking view (a basic black view) that it inserts behind the view being rotated and removes when the rotation is complete.

The implementation of deviceRotated: contains the following code that creates and inserts the blanking view and applies the rotation in response to the UIDeviceOrientationDidChangeNotification:

CGRect windowBounds = self.view.window.bounds;
UIView *blankingView =
    [[[UIView alloc] initWithFrame:
        CGRectMake(-0.5 * (windowBounds.size.height - windowBounds.size.width),
            0, windowBounds.size.height, windowBounds.size.height)] autorelease];
blankingView.backgroundColor = [UIColor blackColor];
[self.view.superview insertSubview:blankingView belowSubview:self.view];

[UIView animateWithDuration:0.25 animations:^{
    self.view.bounds = [self rotatedWindowBounds];
    self.view.transform = [self orientationTransformFromSourceBounds:self.view.bounds];
} completion:^(BOOL complete){
    [blankingView removeFromSuperview];
}];

Conclusion

You can download the ZoomingViewController and the complete sample project used in this post here TapZoomRotate.zip (160kb)

ZoomingViewController is as simple to use as possible: create it, set its view and the view will immediately start responding to taps, zooming to fullscreen and rotating in fullscreen mode. You can apply it to any view in your hierarchy at any time where you need fullscreen display behavior.

Minimalist Cocoa programming

In this post, I build and run a Cocoa Mac application on the command-line. This might not sound like a very difficult goal but I'll attempt to follow an additional constraint: use as few tools, components, classes and even lines of code as possible; truly minimalist Cocoa programming. The goal is to create an application that qualifies as a proper Mac application (including a menubar and a window) but without using Xcode, without an Info.plist file, without NIB files, without Interface Builder and without even using a text editor other than the Terminal itself.

Introduction

For this post, I was inspired (in a somewhat tangential way) by Amit Singh's Crafting a Tiny Mach-O Executable. In his article, Amit Singh crafts a 248 byte Mach-O executable in an effort to make the smallest executable possible on Mac OS X (he also presents a 165 byte version but this no longer runs in Snow Leopard).

A Cocoa application would never really get this small since "Cocoa" implies a large amount of linkage and runtime overhead in the executable (and I don't really want to start coding any of that by hand in assembly). But it inspired me to consider how you might reduce scale and complexity for a Cocoa App; what would be a minimalist Cocoa Mac application?

Of course, the answer in this case is entirely dependent on what criteria you require for a program to be considered a "Cocoa Mac application". I decided that a Cocoa Mac application must:

  • Use NSApplication to run the main event loop
  • Display a menubar with an application menu and a quit item which must correctly terminate the application
  • Display an NSWindow-based window
  • Bring the main window to the front on startup like a normal application
  • Code and program should raise no warnings or errors (preferrably no poor coding practices either but that's subjective)

Ordinarily, these aren't difficult objectives — they are part of the standard Xcode Cocoa application template — but there's actually a huge amount of content described in the default template. Just because you didn't add the different pieces yourself doesn't make it minimalist.

Starting small

If you're extreme enough to believe that you could program directly in hex, then you might consider the following to be the baseline for minimalist Mac OS X programming:

echo "CEFAEDFE07000000030000000200000002000000CC00000000000000010000007C0000005F5F5445585400000000000000000000001000000010000000000000F8000000070000000500000001000000000000005F5F74657874000000000000000000005F5F5445585400000000000000000000E810000010000000E80000000200000000000000000000000000000000000000000000000500000050000000010000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000E8100000000000000000000000000000000000000000000031C040682A00000081EC04000000CD80" | xxd -r -p - tiny; chmod a+x tiny; ./tiny; echo $?

That's Amit Singh's 248-byte program in raw hex, saved to a file by "xxd", marked as executable using "chmod", run and the result displayed (it returns the exit condition 42).

Realistically though, all I can read are the first 5 bytes "CEFAEDFE07" which are the magic value for a Mach-O executable plus the CPU type for X86 CPUs. While historically, people certainly have programmed in hex, I would never write a Mac OS X program like this and I doubt anyone else would either. So piping Amit's actual assembly directly through nasm is probably a more realistic baseline for minimalist Mac OS X programming.

I'm not going to fight through this in assembly though (it's an Objective-C blog, after all), so I'm going to need a compiler to progress from here.

Creating, building and running the smallest possible C program looks something like this:

echo "main(){}" | gcc -x c - ; ./a.out; echo $?

The options used with gcc might look a bit odd but they are: "-x c" (build as C code) and "-" (take input on standard in). The default output file name of "a.out" is acceptable for now.

While technically this builds a valid program and looks simpler than the block of hex up above, it actually does less than the previous program did — it doesn't even deliberately return a specific value (instead I get 252 which is probably just junk from the stack since main implicitly returns an int that we aren't returning).

It's a little better to actually return a value. While it's not much, you can at least claim that the program "does something".

echo "int main(){return 0;}" | gcc -x c -; ./a.out ; echo $?

Great, we've returned the default success value.

Tiny Cocoa

The smallest Objective-C program is the same as the smallest C program, just change the command-line so gcc builds Objective-C:

echo "int main(){return 0;}" | gcc -x objective-c -; ./a.out ; echo $?

but the gcc output is literally the same. I'll need to use at least one class to truly claim this is Objective-C:

echo "#import <objc/Object.h>
int main(){return [Object class]==nil;}" | gcc -x objective-c -lobjc -; ./a.out ; echo $?

Making a Foundation program is no harder, we just link against the Foundation framework instead of libobjc. I could use literally the same program but here's one with a proper NSAutoreleasePool, logging and process information:

echo '#import <Foundation/Foundation.h>
int main(){
    id pool=[NSAutoreleasePool new];
    NSLog(@"%@", [[NSProcessInfo processInfo] arguments]);
    [pool drain];
    return 0;
}' | gcc -framework Foundation -x objective-c - ; ./a.out ; echo $?

Notice that NSProcessInfo has no problems getting the command-line arguments without main needing to handle them.

With an AppKit application, the main function looks a little simpler because -[NSApplication run] creates its own autorelease pool (although I'll need to bring it back again later to work outside -[NSApplication run]):

echo '#import <Cocoa/Cocoa.h>
int main(){
    [[NSApplication sharedApplication] run];
    return 0;
}' | gcc -framework Cocoa -x objective-c - ; ./a.out ; echo $?

Unfortunately, while this AppKit program runs, it doesn't present a user-interface and doesn't quit unless we kill it. Not very satisfying.

Satisfying the requirements

In Snow Leopard, programs without application bundles and Info.plist files don't get a menubar and can't be brought to the front unless the presentation option is changed:

[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];

Next, we need to create the menu bar. You don't need to give the first item in the menubar a name (it will get the application's name automatically):

id menubar = [[NSMenu new] autorelease];
id appMenuItem = [[NSMenuItem new] autorelease];
[menubar addItem:appMenuItem];
[NSApp setMainMenu:menubar];

Then we add the quit item to the menu. Fortunately the action is simple since terminate: is already implemented in NSApplication and the NSApplication is always in the responder chain.

id appMenu = [[NSMenu new] autorelease];
id appName = [[NSProcessInfo processInfo] processName];
id quitTitle = [@"Quit " stringByAppendingString:appName];
id quitMenuItem = [[[NSMenuItem alloc] initWithTitle:quitTitle
    action:@selector(terminate:) keyEquivalent:@"q"] autorelease];
[appMenu addItem:quitMenuItem];
[appMenuItem setSubmenu:appMenu];

Finally, all we need to do is create a window and activate the application:

id window = [[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 200, 200)
    styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]
        autorelease];
[window cascadeTopLeftFromPoint:NSMakePoint(20,20)];
[window setTitle:appName];
[window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];

In keeping with the pattern of the rest of the post, here's the entire program as a single, command-line executable statement:

echo '#import <Cocoa/Cocoa.h>
int main ()
{
    [NSAutoreleasePool new];
    [NSApplication sharedApplication];
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
    id menubar = [[NSMenu new] autorelease];
    id appMenuItem = [[NSMenuItem new] autorelease];
    [menubar addItem:appMenuItem];
    [NSApp setMainMenu:menubar];
    id appMenu = [[NSMenu new] autorelease];
    id appName = [[NSProcessInfo processInfo] processName];
    id quitTitle = [@"Quit " stringByAppendingString:appName];
    id quitMenuItem = [[[NSMenuItem alloc] initWithTitle:quitTitle
        action:@selector(terminate:) keyEquivalent:@"q"] autorelease];
    [appMenu addItem:quitMenuItem];
    [appMenuItem setSubmenu:appMenu];
    id window = [[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 200, 200)
        styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]
            autorelease];
    [window cascadeTopLeftFromPoint:NSMakePoint(20,20)];
    [window setTitle:appName];
    [window makeKeyAndOrderFront:nil];
    [NSApp activateIgnoringOtherApps:YES];
    [NSApp run];
    return 0;
}' | gcc -framework Cocoa -x objective-c -o MinimalistCocoaApp - ; ./MinimalistCocoaApp

That's an entire Cocoa Mac application that you can copy onto the clipboard, paste directly at the prompt in Terminal, hit return and it'll run.

Conclusion

At 9352 bytes when compiled (exact size may change from computer to computer), my simple Cocoa program is nowhere near as small as the assembly written Mac OS X baseline of 248 bytes. At 27 lines long, it might not immediately seem "minimalist", either. However, it does satisfy the requirements I set for being a proper Cocoa Mac application.

The reality is that the things done under-the-hood by the default application templates, by the Xcode build system, by Interface Builder and by NSApplication itself in conjunction with the Info.plist file are significant. To replicate the required subset of their functionality requires at least some work. Compared to the standard Xcode Cocoa application template's 85782 bytes for the entire compiled bundle, my app's goal of Cocoa minimalism seems far more successful.

Reducing the size of the executable wasn't my primary goal, though. I was aiming to reduce the number of tools, components, files and classes required for a genuine Cocoa Mac application. Relative to the standard suite of tools and collection of files involved in building a Cocoa Mac app, 27 lines of code processed using echo, gcc and a bash terminal represents a significant simplification.

I welcome further suggestions about how to satisfy the same requirements with less.

The overhead of spawning threads (a performance experiment)

In this post, I take a casual look at the relative performance overheads handling tasks in different ways: performing all tasks in the main thread, sending tasks to a single worker thread, spawning new threads for every task, and using Grand Central Dispatch (GCD). This won't be a particularly advanced investigation, simply a quick overview of simplicity versus performance in job management.

Introduction

I have a number of projects that need to support Mac OS X Leopard and iOS 3.x, so these projects cannot use libdispatch (aka Grand Central Dispatch).

In these GCD-free situations, I was curious to know what was the difference in overhead between properly setting up dedicated CFRunLoop-based worker threads and using a more haphazard spawn-a-new-NSThread-for-every-task approach. This post presents the results of that investigation.

I also included GCD results to compare with these traditional Cocoa threading approaches. There should be no surprises that GCD is much faster than any of the other threaded approaches. However there is also some interesting information regarding GCD queue configurations that can affect performance on different computers.

Test setup

The test is relatively straightforward:

  • A number of job queues need to run.
  • Each queue runs a number of jobs serially.
  • The complete set of jobs are not known in advance — as each job completes, it adds the next job to the queue.

In the code, I've called the number of jobs that run on each queue "iterations" because there is actually only one job object per queue and it adds itself back to the queue as the next job.

The jobs themselves have no work to perform (other than decrementing the iteration count). The purpose here is purely to test the overhead of different job queuing and management approaches.

Queue implementations

SingleThreadedQueue

The single threaded queue simply adds all jobs to an NSMutableArray and runs them in the current thread in the order added.

This test involves no worker thread at all and is really the "control" case.

Running all the jobs involves looping until the queue is empty and running one job per iteration.

- (void)queueJob:(Job *)aJob
{
    if (!jobQueue)
    {
        jobQueue = [[NSMutableArray alloc] init];
        [jobQueue addObject:aJob];
        
        while ([jobQueue count] > 0)
        {
            Job *nextJob = [jobQueue objectAtIndex:0];
            [jobQueue removeObjectAtIndex:0]; 
            [nextJob performIterationAndRequeueInJobRunner:self];
        }
        
        [jobQueue release];
        jobQueue = nil;
    }
    else
    {
        [jobQueue addObject:aJob];
    }
}

The if (!jobQueue) condition around the other work exists to avoid recursion on the stack when the performIterationAndRequeueInJobRunner: method invokes queueJob: again to queue the next job.

RunLoopQueue

Running a dedicated worker thread is involves creating and starting an NSThread but then once the thread is started, you can add jobs to it with performSelector:

- (void)queueJob:(Job *)aJob
{
    [aJob
        performSelector:@selector(performIterationAndRequeueInJobRunner:)
        onThread:runLoopThread 
        withObject:self
        waitUntilDone:NO];
}
DetachThreadQueue

The main point here is that we create a new thread for every job. We also need to have a thread entry point that puts an NSAutoreleasePool up (or other thread context that we may require) and runs the actual job itself.

- (void)queueJob:(Job *)aJob
{
    [NSThread detachNewThreadSelector:@selector(threadEntry:)
        toTarget:self withObject: aJob];
}
- (void)threadEntry:(Job *)aJob
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [aJob performIterationAndRequeueInJobRunner:self];
    [pool release];
}
DetachThreadWithVerificationQueue

However, when experimenting with this test, I ran into an issue I'd never encountered before: the Mac OS X thread limit. According to the command line function sysctl kern.maxfiles my thread limit is around 12288 for the whole operating system (the limit is RAM dependent) — you do have to be pretty reckless to use them all but it's not impossible.

The annoying point here is that you don't get an error when NSThread fails to start an actual thread — instead, the thread never starts and you're left wondering why nothing happened.

So I introduced a little extra code to ensure that the thread started correctly. This code passes an NSCondition into the detached thread and if this condition isn't signalled within 10 seconds, it is assumed that the thread failed to launch.

- (void)queueJob:(Job *)aJob
{
    NSCondition *startedCondition = [[NSCondition alloc] init];
    NSDictionary *threadParameters =
        [NSDictionary dictionaryWithObjectsAndKeys:
            aJob, @"job",
            startedCondition, @"condition",
        nil];
    
    [startedCondition lock];
    
    [NSThread detachNewThreadSelector:@selector(threadEntry:)
        toTarget:self withObject:threadParameters];
    
    if (![startedCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]])
    {
        NSLog(@"Thread creation failed.");
        [aJob killJob];
    }
    
    [startedCondition unlock];
    [startedCondition release];
}
- (void)threadEntry:(id)threadParameters
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSCondition *startedCondition = [threadParameters objectForKey:@"condition"];
    Job *aJob = [threadParameters objectForKey:@"job"];
    
    [startedCondition lock];
    [startedCondition signal];
    [startedCondition unlock];
    
    [aJob performIterationAndRequeueInJobRunner:self];
    [pool release];
}
GCD Dedicated Queue

Once you've created a queue using dispatch_queue_create, sending a job to it is very simple:

- (void)queueJob:(Job *)aJob
{
	dispatch_async(queue, ^{
		[aJob performIterationAndRequeueInJobRunner:self];
	});
}
DispatchGlobalConcurrentQueue

The implementation of queueJob is identical here, the only difference is that the queue is obtained using dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0).

- (void)queueJob:(Job *)aJob
{
	dispatch_async(queue, ^{
		[aJob performIterationAndRequeueInJobRunner:self];
	});
}

Results

My computer is a 4 core Mac Pro with HyperThreading. This is particularly relevant for these tests because it should be able to support 8 threads in hardware. Let's see how it goes.

In the following table of timing results, the following abbreviations are used:

  • Single — SingleThreadedQueue
  • RunLoop — RunLoopQueue
  • Detach — DetachThreadQueue
  • Detach w/ Ver. — DetachThreadWithVerificationQueue
  • GCD-DQ — DispatchDedicatedQueue
  • GCD-GCQ — DispatchGlobalConcurrentQueue
Configuration Single RunLoop Detach Detach w/ Ver.GCD-DQ GCD-GCQ
1 Queue,
100k iterations
0.035990 0.776727 6.356978 7.166419 0.052294 0.102622
4 Queues,
25k iterations
0.036177 0.243689 4.513922 4.643964 0.038666 0.044127
8 Queues,
12.5k iterations
0.036134 0.199367 13.750981 11.947684 0.025748 0.046173
16 Queues,
6.25k iterations
0.036132 0.200769 40.493681 30.934207 0.025616 0.046114

All times are in seconds. Iterations are per queue (total iterations is always 100,000).

Visually comparing all except the Detach Thread approaches:

chart1.png

The vertical axis is in seconds.

I deliberately cut the top of the graph off but the RunLoop version took 0.776727 in the first test — more than three times above the top of this graph.

chart2.png

I've placed the Detach Thread approaches on their own graph because they're more than an order of magnitude slower. Again, the vertical axis is in seconds.

Analysis

Since these tests were intended to test job queue overhead, and the SingleThreadedQueue had no threading overhead and only need to perform NSArray operations, it is unsurprising that it was generally the fastest — except in the 8 and 16 queue cases where the DispatchDedicatedQueue was faster (likely because the little overhead it actually has is absorbed by the multiple cores in my computer).

My computer can run 8 threads in hardware. This likely explains why 8 queues is the optimum number of RunLoops to spawn and GCD dedicated queues. However — the GCD global concurrent queue never used all 8 possible threads (it peaked at 59% total CPU usage which is approximately 4 threads used), so it peaked in performance at 4 concurrent queues.

The DetachThread queues actually used twice as many threads as queues (the previous and next jobs' queues are both present at the same time) so these queues peaked at 4 queues. Interestingly, the "with verification" version started to exceed the performance of the "without" version after 4 queues — I suspect this is because the mutex used in the verification actually reduced the active thread count slightly.

Clearly, detaching threads has a very high overhead — around 1 second per 12,000 threads spawned. This isn't going to be an issue if you're only spawning 20 or 30 threads but spawning hundreds or thousands is a complete waste of time — and it only gets worse as the number of active threads at any given time increases.

Conclusion

You can download the code used in this post ThreadingOverheads.zip (14kb)

While it is valid to detach new threads for infrequent tasks (as many as dozens per second), the overhead on a completely new thread is non-trivial so if your tasks are small and numerous, a solution that reuses threads is pretty important. Even the overhead of a RunLoop solution (which was the traditional worker thread approach in Cocoa prior to GCD) is noticeable once the number of tasks reaches the tens of thousands.

It is easy to see why Apple chose to introduce Grand Central Dispatch — it lowers the overhead on job queues by an order of magnitude relative to a typical RunLoop-based worker thread — and they're easier to create and use as well.

Substituting local data for remote UIWebView requests

In this post, I'll show you how you can load a webpage in a UIWebView in iOS while using a modified NSURLCache to substitute local copies of resources within the webpage for the remote copies referred to by the actual page.

Introduction

Normally if you're writing an iOS app with network connectivity, you'll want to put a native iOS interface on all data received over the network.

However, there are always scheduling and other constraints on a project that limit what you can implement and sometimes you may simply choose to show a regular, webpage to the user.

If you choose to take this approach, it is best to make sure the web interface feels as smooth as possible. One of the steps you can take to ensure this is to include local copies of all image and other non-updating resources within the application itself.

To use a local resource in an iOS webpage loaded from a remote location, either the remote page must refer to the local resource in some way (e.g. through a custom URL scheme) or you must swap a local location in place of a remote locations.

In this post, I'll look at how we can substitute a local resource when the webpage contains references to remote resources.

NSURLCache

On the Mac, you could use a range of different approaches in the WebViewDelegate to do this, including implementing webView:resource:willSendRequest:redirectResponse:fromDataSource: to substitute one NSURLRequest for another. Unfortunately, the UIWebViewDelegate in iOS is not nearly as capable so we need to do this another way.

Fortunately, there is one point you can hook into that is invoked for (almost) every request: the NSURLCache.

Normally, very little is actually cached in the NSURLCache, particularly on older iOS devices where the cache size is downright miniscule. Even if you use the setMemoryCapacity: method to increase the size of the cache, it seems significantly less likely to store resources than the NSURLCache on the Mac.

Of course that doesn't matter in this case, since we're going to subclass NSURLCache and implement our own version that will be guaranteed to hold all the resources we need and won't need pre-caching (all the resources will be there before the program is started).

cachedResponseForRequest:

The only important method we need to override is cachedResponseForRequest:. This will allow us to examine every request before it is sent and return local data if we prefer.

For this code, I'll use a dictionary that maps remote URLs to local file names in the Resources folder of the application bundle. If any request is made for the specified URLs, the contents of the local file will be returned instead.

So given the following dictionary containing a single path for substitution:

- (NSDictionary *)substitutionPaths
{
    return
        [NSDictionary dictionaryWithObjectsAndKeys:
            @"fakeGlobalNavBG.png",
            @"http://images.apple.com/global/nav/images/globalnavbg.png",
        nil];
}

The following cachedResponseForRequest: implementation will substitute the contents of the fakeGlobalNavBG.png file in the Resources folder any time the URL http://images.apple.com/global/nav/images/globalnavbg.png is requested.

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
    // Get the path for the request
    NSString *pathString = [[request URL] absoluteString];
    
    // See if we have a substitution file for this path
    NSString *substitutionFileName = [[self substitutionPaths] objectForKey:pathString];
    if (!substitutionFileName)
    {
        // No substitution file, return the default cache response
        return [super cachedResponseForRequest:request];
    }
    
    // If we've already created a cache entry for this path, then return it.
    NSCachedURLResponse *cachedResponse = [cachedResponses objectForKey:pathString];
    if (cachedResponse)
    {
        return cachedResponse;
    }
    
    // Get the path to the substitution file
    NSString *substitutionFilePath =
        [[NSBundle mainBundle]
            pathForResource:[substitutionFileName stringByDeletingPathExtension]
            ofType:[substitutionFileName pathExtension]];
    NSAssert(substitutionFilePath,
        @"File %@ in substitutionPaths didn't exist", substitutionFileName);
    
    // Load the data
    NSData *data = [NSData dataWithContentsOfFile:substitutionFilePath];
    
    // Create the cacheable response
    NSURLResponse *response =
        [[[NSURLResponse alloc]
            initWithURL:[request URL]
            MIMEType:[self mimeTypeForPath:pathString]
            expectedContentLength:[data length]
            textEncodingName:nil]
        autorelease];
    cachedResponse =
        [[[NSCachedURLResponse alloc] initWithResponse:response data:data] autorelease];
    
    // Add it to our cache dictionary for subsequent responses
    if (!cachedResponses)
    {
        cachedResponses = [[NSMutableDictionary alloc] init];
    }
    [cachedResponses setObject:cachedResponse forKey:pathString];
    
    return cachedResponse;
}

Setting our cache as the shared cache

A UIWebView will try to use the current +[NSURLCache sharedURLCache]. To get our code called, you'll need to create an instance of our NSURLCache subclass and invoke +[NSURLCache setSharedURLCache:].

A big warning here: once you set a new web cache, you probably want to leave it set until your program exits.

When the UIWebView requests resources from your NSURLCache, it assumes that the NSURLCache retains the NSCachedURLResponse. If you release the NSCachedURLResponse while any UIWebView is using it, it will probably crash your app.

Unfortunately, it is pretty hard to force WebKit to let go of its references — it can hold onto them indefinitely in some cases. Until WebKit itself chooses to invoke removeCachedResponseForRequest: to tell you that you can throw away the resource you must hold onto it.

What this means is that you should only have one NSURLCache in your program. Set it in your application:didFinishLaunchingWithOptions: method and never remove it.

A limitation...

Obviously, if you're overriding the cache to substitute local data, it will only work if the request actually looks at the cache.

This means that if the URL is requested with requestWithURL:cachePolicy:timeoutInterval: with a cache policy of NSURLRequestReloadIgnoringCacheData, the the request will bypass this local substitution.

By default, NSURLRequests have a cache policy of NSURLRequestUseProtocolCachePolicy. The HTTP cache policy is pretty complicated and while I've never actually seen a normal NSURLRequest actually bypass the cache, the number of rules involved create a situation where it seems like it may be possible in some situations. Your app should not misbehave if this were to happen for some reason.

The LocalSubstitutionCache sample app

You can download the LocalSubstitutionCache.zip (66kb) sample project

Here's a small screenshot of today's http://www.apple.com running in a UIWebView:

Screen shot 2010-09-06 at 9.37.47 PM.png

After invoking +[NSURLCache setSharedURLCache:] with our NSURLCache subclass, the gray links bar across the top are replaced with a blue graphic stored in the app's bundle:

Screen shot 2010-09-06 at 9.38.39 PM.png

Conclusion

The purpose of this work is to allow UIWebViews to feel more responsive and a bit more like native user-interfaces.

In reality, a UIWebView will never feel as responsive or integrated as a native user-interface but sometimes making one screen of your app a remote webpage is a big enough saving in developer resources that you're prepared to make the sacrifice in user quality. Making sure as many resources as possible are stored locally will help make any negative impact on user quality as minor as possible.