Misconceptions About iOS Multitasking

There is one iOS "tip" that I keep hearing and it is wrong. Worse, I keep hearing it from supposedly authoritative sources. I have even heard it from the lips of Apple "Geniuses" in stores.

Here is the advice - and remember it is wrong:

All those apps in the multitasking bar on your iOS device are currently active and slowing it down, filling the device's memory or using up your battery. To maximise performance and battery life, you should kill them all manually.

Wrong. Wrong. Wrong. Wrong. Wrong. Wrong. Wrong. There are caveats to this but anyone dispensing the advice above is clearly uninformed enough that they will certainly not be aware of these subtleties.

Let me be as clear as I can be: the iOS multitasking bar does not contain "a list of all running apps". It contains "a list of recently used apps". The user never has to manage background tasks on iOS.

Except in a few cases, which I'll explain, the apps that appear in the multitasking bar are not currently running. When you press the home button, iOS will tell the app to quit. In almost all cases, it quits, it stops using CPU time (and hence battery) and the memory it was using is eventually recovered if required.

Let's get technical: iOS apps have five states of execution. These are:

  • Not running - the app has been terminated or has not been launched.
  • Inactive - the app is in the foreground but not receiving events (for example, the user has locked the device with the app active)
  • Active - the normal state of "in use" for an app
  • Background - the app is no longer on-screen but is still executing code
  • Suspended - the app is still resident in memory but is not executing code

Active and Inactive are not interesting for this discussion. Most of the confusion is around what happens as an app goes from Active to Background to Suspended to Not Running.

When you press the home button, the app moves from Active to Background. Most apps usually then go from Background to Suspended in a matter of seconds.

The first technical caveat is that Suspended apps remain in the device's memory. This is so they can resume more quickly when you go back to them. They're not using processor time and they're not sucking battery power.

You may think that, if an app is resident in memory, you have to somehow remove it to conserve memory. You don't because iOS does it for you. If there are Suspended apps lying around and you launch a memory-intensive app such as a big game, iOS will start to purge Suspended apps and move them to the Not Running state. That is, they will be completely removed from memory and will launch afresh the next time you tap their icon.

Where some people get confused is this: all of the above has no impact on what you see in the multitasking bar. The multitasking bar always shows a list of recently used apps, regardless of whether they're in the Background, Suspended or Not Running states. You may also have noticed that the app that is currently Active does not appear in the multitasking bar.

Background Tasks

When an app is sent to the Background, it usually moves to the Suspended state in a few seconds. An app can request an extension to this by declaring that it's starting a "background task".

A good example is an app that downloads largish files from the web such as Instacast, my favourite podcast app. When Instacast is Active, it can start to download new podcasts. If I then hit the home button on my iPhone, Instacast gets five seconds to run in the Background state and then it's Suspended. That interrupts the download of my podcasts, which might take 5 minutes or more.

iOS allows Instacast to declare that a download is a "background task". This allows Instacast an extra period of background running after I hit the home button to complete the podcast download. While apps can request additional Background time, that time is not infinitely long. The app gets about 10 minutes of Background running time and then it is forcibly suspended by iOS. Again, you don't have to worry about this yourself.

Indefinite Background Running

All apps get 5 seconds of Background running. Some apps can request a 10-minute extension. There are a small number of apps that genuinely need to run indefinitely in the background and iOS allows this.

There are exactly five kinds of apps allowed to run indefinitely in the Background state in iOS 5:

  • Apps that play audio while in the Background state. A good example is Instacast while it's playing a podcast.
  • Apps that track your location in the Background. For example, you still want voice prompts from your TomTom navigation app, even if another app is Active.
  • Apps that listen for incoming VOIP calls. If you use Skype on iOS, you can receive incoming Skype calls while the app is in the Background.
  • Newsstand apps that are downloading new content.
  • Apps that receive continuous updates from an external accessory in the Background.

All well-written apps in the above categories should become Suspended when they are no longer performing the task in hand. When Instacast finishes playing a podcast, it should be Suspended. There are some built-in apps that also run continuously in the background on iOS - the most-used one is probably Mail.

As long as these apps are running in the Background state, they will consume memory, CPU time and power. In general, though, you would know that you were using such apps. The developer has to declare which category of Background running they require and part of the App Store review process is to check that these declarations are not being abused.

I said earlier that "the user never has to manage Background tasks on iOS". The only exception to this is when one of these Background-running apps goes berserk and will not terminate properly. That, however, is an exceptional situation and not a normal part of being an iOS user.

Summary

Let me wrap this up by giving you a quick summary:

  1. If someone tells you that all the apps in the multitasking bar are running, using up memory or sucking power, they are wrong.
  2. When you hit the home button, an app moves from Active to Background and quickly to the Suspended state where it no longer uses CPU time or drains power.
  3. An app may request an additional 10 minutes of Background running to complete a big task before becoming Suspended.
  4. If memory is becoming scarce, iOS will automatically move Suspended apps into the Not Running state and reclaim their memory.
  5. Five classes of apps - audio, GPS, VOIP, Newsstand and accessory apps - and some built-in apps such as Mail may run indefinitely in the background until they complete their task.

Put simply: you do not have to manage background tasks on iOS. The system handles almost every case for you and well written audio, GPS, VOIP, Newsstand and accessory apps will handle the rest.

Moving a Mac App: Viewfinder for iPad

Viewfinder for iPad shipped a couple of weeks ago. It's been insanely busy since then but I finally found some time to reflect on the process of moving a Mac OS X application to iPad.

Mission Statement

In most talks you hear from Apple employees about designing for iPhone and iPad, they always suggest that you develop a 'mission statement' for your app. On Mac OS X, Viewfinder's mission statement was simply:

Easy photo search and download for Mac OS X

For the iPad version, that statement was radically overhauled to say:

Easy photo search and download for iPad

The entire point of Viewfinder has always been to solve the problem that it's way too hard to find and use good quality, correctly-licensed photos on the internet.

Translation

When moving an application or its general concept to iPhone, the rule has always been "find the core functionality and translate that to the iPhone". It's still true of the iPad but the difference is that the iPad increased processing power and screen size invites you to bring a much larger feature set.

As I was working on bringing the core functionality of Viewfinder to the iPad, I took a three-step approach:

  • Eliminate the features that are impossible to translate
  • Simplify the features that can be done, but in a reduced manner
  • Focus on a polished, native experience for the rest

Eliminate the Impossible

As it stands, Viewfinder on Mac OS X is a fairly tightly focused application. It doesn't contain a lot of superfluous features, but there were a few features that couldn't be brought to iPad because of the limitations of the iOS platform.

Viewfinder on Mac OS X can automatically add a downloaded photo to the current Keynote document. This feature is implemented on Mac OS X using Automator, which doesn't exist on iOS, so it couldn't be brought over. There's no equivalent cross-application automation technology on iOS, so it had to go.

Similarly, the feature that allows Viewfinder to set the Mac OS X desktop picture directly is also not possible on the iPad. There's simply no API to do this on iOS. Gone.

Finally, Viewfinder supports a "Search in Viewfinder" service on Mac OS X. As with the other features, Mac OS X services have no analogue on iOS so the feature had to go.

Similar but Simpler

Developing for iOS devices is developing a highly resource-starved environment. Many fewer, slower processor cores; slower graphics hardware, dramatically less RAM and, on 3G, much slower and less reliable networking. I find it a source of constant wonder that we squeeze the performance that we do out of these devices, and often wonder why Mac OS X machines feel so slow by comparison.

Viewfinder is an application designed to show large numbers of photos. Without care, this is potentially a difficult thing to do. Photos are some of the largest data blobs an iOS app will handle, so it's important to manage that precious memory carefully.

On the Mac, Viewfinder has a dynamically-resizable thumbnail view. It responds to the user's intention by automatically loading ever-higher resolution images as the thumbnails grow. The internal architecture of this feature is something I'm really proud of, and I think it's a great feature.

The idea of transparently hitting the network for ever larger images doesn't translate well to the iPad. Particularly in this new world of mobile data caps which are "unlimited*" - the asterisk being the international symbol for "if you take this literally you're a sucker" - being more conservative with data usage is important.

So, for Viewfinder on the iPad, I built a thumbnail view with similar core functionality only simpler. The thumbnails are fixed at a maximum width or height of 100px and, instead of zooming them to a larger size, you enter a full-screen mode similar to the built-in Photos application.

Another feature that I personally love in Viewfinder for Mac OS X is the ability to filter the search results by the size of the largest available version. This is a really important feature on the desktop, where you may be looking for photos to fill a 27" display or a high-resolution document.

On the iPad, I decided to leave this feature out. I left it out for two reasons. One is that using the original full-size image on the iPad is often difficult because of the incredibly large file size that some modern DSLRs produce. Secondly, you don't need a massively high-resolution image for most purposes on the iPad. The screen is only 1024x768. Viewfinder still supports searching by the full range of Creative Commons licenses.

Polish The Rest

So, what's left in Viewfinder? There are a core set of tasks that make the application:

  • Set search options
  • Begin a Search
  • View a set of thumbnails
  • View a larger size
  • Download photos
  • Inspect and copy information about the photo

Too many iOS applications aspire to "brand" themselves. It's sometimes appropriate, of course, but I took my inspiration from two system apps that Apple provides: Safari and Maps.

The thing I most love about Maps is that there is almost no UI. It's an incredibly powerful application that delivers everything through a small number of UI elements:

  • A toolbar with two buttons and a search field (two fields in Directions mode).
  • A popover with details about locations
  • A page-curl view to set options
  • An alternate full-screen UI for Street View

The rest of the display in Maps is given over to the main function - showing a map. Viewfinder endeavours to be similarly minimalist.

Not the iPhone

The typical approach to bringing an application to the iPhone was really three steps:

  • Identify the core functionality of the app
  • Remove everything else that isn't essential
  • Polish the user experience

I think the iPad is different. I've written before that I believe the iPad is a true productivity platform and not just the "content consumption" toy that some lazily claim.

Instead of looking for some kind of barebones set of core functionality, I decided to see how much of the desktop app I could bring to the iPhone. I'm delighted with the result and I encourage my fellow developers to push the iPad as far as it can go.

For the rest of you, why not visit the App Store and check out Viewfinder for iPad?

Using Instruments to Learn About NSCache

I recently came across the Foundation class NSCache, which was introduced in Mac OS X 10.6 and now in iOS 4. NSCache is a key-value store with an API similar to NSDictionary, except that it will automatically evict objects from its store when the 'cost' (however defined) of the cache rise above a configurable threshold.

Instead of writing a thousand words about using Instruments to explore the behaviour of a class, I decided to make a screencast instead so here, in six minutes, is a quick discussion of NSCache and an example of using Instruments to explore its behaviour in terms of memory footprint.

PS: this is a small size to fit on the blog. It might be better to watch a larger version at YouTube.

HFS Promise Drags from IKImageBrowserView

One of the pieces of hard-won knowledge in Viewfinder is the ability to drag from the thumbnail grid to the Finder to initiate a download. This is known as an "HFS Promise" drag and is widely used in Cocoa to allow dragging and dropping of files that do not yet exist.

Viewfinder uses this to allow drag-drop of files that have not yet been downloaded.

At first glance, it seems hard to do this with IKImageBrowserView, because of the way that class interacts with its delegate to get dragged items onto the pasteboard.

To perform HFS Promise drags out of IKImageBrowserView, you have to get a little deeper and subclass IKImageBrowserView.

Interface

Firstly, here's the interface declaration:

@interface VFImageBrowserView : IKImageBrowserView {
    NSArray *itemsInCurrentPromiseDragOperation;
    BOOL dragSelectInProgress;
}
@end

I keep two variables around:

  • itemsInCurrentPromiseDragOperation - to store the items that are being promise-dragged.
  • dragSelectInProgress - is set to YES when I detect that the user is performing a drag-select.

The definition of a 'drag select' being in progress in this case is: the period between a mouseDown event whose point is not over any image in the browser and the next mouseUp event.

-mouseDown:

In -mouseDown:, I check to see if the user has clicked on an image in the browser, or on the background. To do that, I get the clickPosition of the event, then query self for the index of the image that's under that point. If there's no image (i.e. the index is NSNotFound), I assume the user is doing a drag selection. Then I let super handle the event.

- (void)mouseDown:(NSEvent *)theEvent {
    // If the mouse first goes down on the background, this is a drag-select and
    // we don't want to handle any mouseDragged events until the mouse comes up 
    // again.
    NSPoint clickPosition = [self convertPoint:[theEvent locationInWindow]
                                      fromView:nil];
    NSInteger indexOfItemUnderClick = [self indexOfItemAtPoint: clickPosition];
    dragSelectInProgress = (indexOfItemUnderClick == NSNotFound);
    [super mouseDown: theEvent];
}

-mouseUp:

The implementation of -mouseUp: is simple. Set the dragSelectInProgress variable to NO and let super handle the rest:

- (void)mouseUp:(NSEvent *)theEvent {
    dragSelectInProgress = NO;
    [super mouseUp: theEvent];
}

-mouseDragged:

The real action happens in the override of -mouseDragged:. Firstly, if a drag-select is in progress, we pass the event to super and ignore. If that's not the case, we check for an item under the click point. Again, if there's none, we pass to super and ignore.

Next, we use some Snow Leopard block goodness to store the currently selected items into the itemsInCurrentPromiseDragOperation array and finally call -dragPromisedFilesOfTypes:fromRect:source:slideBack:. This sets up the promise drag.

- (void)mouseDragged:(NSEvent *)theEvent;
{
    // If there's a drag-select in progress, we don't want to know.
    if(dragSelectInProgress) {
        [super mouseDragged: theEvent];
        return;
    }

    // Otherwise, the mouse went down on an image and we should drag it
    NSPoint dragPosition = [self convertPoint:[theEvent locationInWindow] 
                                     fromView:nil];
    NSInteger indexOfItemUnderClick = [self indexOfItemAtPoint: dragPosition];

    if(indexOfItemUnderClick == NSNotFound) {
        [super mouseDragged: theEvent];
        return;
    }

    // Store the selected browser items
    __block NSMutableArray *tempItems = [NSMutableArray array];
    [[self selectionIndexes] enumerateIndexesUsingBlock:
    ^(NSUInteger idx, BOOL *stop) {
        [tempItems addObject: [self.dataSource imageBrowser: self itemAtIndex: idx]];
    }];
    itemsInCurrentPromiseDragOperation = tempItems;

    dragPosition.x -= 16;
    dragPosition.y -= 16;

    NSRect imageLocation;
    imageLocation.origin = dragPosition;
    imageLocation.size = NSMakeSize(64, 64);

    [self dragPromisedFilesOfTypes:[NSArray arrayWithObject:@"jpg"]
                          fromRect:imageLocation
                            source:self
                         slideBack:YES
                             event:theEvent];
}

-namesOfPromisedFilesDroppedAtDestination:

Finally, we have to implement -namesOfPromisedFilesDroppedAtDestination:, which is called once the Finder accepts the drag. All we need to do is assemble a list of file names and return it. I also set the itemsInCurrentPromiseDragOperation variable to nil.

- (NSArray *)namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination;
{
    __block NSMutableArray *fileNames = [NSMutableArray array];

    [itemsInCurrentPromiseDragOperation enumerateObjectsUsingBlock:^(VFBrowserItem *obj, NSUInteger idx, BOOL *stop) {
        // Here, do whatever action makes sense for your application.
        // Viewfinder kicks off a download here.    
        // In this block, you have to insert a file name into the fileNames 
        // array.
    }];

    itemsInCurrentPromiseDragOperation = nil;
    return fileNames;
}

Note that this code is written for 10.6 using garbage collection. I hope this is useful.