So, one of the most common complaints about PCalc before 1.7 is that it took too long to launch – around four seconds on an iPhone 1st Generation. I actually got emails accusing me of having a massive ego because I was making the splash screen stay up for so long. Now, I may have a massive ego anyway, but that’s not why it was happening.
While an iPhone app is being loaded by the OS, it displays a fixed image known as the “Default.png”. The idea is that this image looks close to how your application will appear when it is running, so the user thinks your app has launched quicker than it actually has. This image is fixed at the point the application is built, it can’t be changed by the developer afterwards (some of the Apple apps do change it, but that’s another story). This approach doesn’t work well for many applications.
Firstly, if your application can be configured in different ways, the default image isn’t going to match and it will be jarring to the users when it changes. In the initial release of PCalc, I used a “Default.png” which looked like the default vertical layout of the calculator with the screen dimmed.
In later versions of PCalc I added the different key layouts and the themes, so the initial state of the calculator could look very different to the default image. So, I got complaints – people thought I was loading the calculator twice, once with the defaults and once with their settings. It felt unnecessary and confusing.
The other problem is that if your application takes a long time to start up, and the default image looks too much like the running application, people will tap away on it, thinking that it’s already launched. Nothing will happen and they will be frustrated. This also happened with PCalc.
So, I changed the default image to be a nice splashscreen with a big version of the logo like so:
Problem solved? No. The reason Apple used this default image technique (taken from Dashboard widgets) is to make the apps feel like they were launching faster. Now there was no jarring transition, but people stared at my splashscreen for four seconds before the calculator appeared and it felt like an eternity while they were waiting to start typing. And some people thought I just wanted my name in lights. As I added more and more features to PCalc, the startup got slower and slower, and people started complaining again.
Ok, so how did I fix this, and how can you do something similar?
Firstly, you need to be smarter about what work you do when your application initialises. PCalc was already pretty optimised – I don’t build the views for the horizontal layout of the calculator until you actually rotate it for example – but there was still a lot happening. The theme engine I took from DragThing is pretty complex – all the buttons are drawn programatically from a description in an XML file. I cache lots of images so I never draw the same thing twice, so for example, once I’ve drawn the background of one button, I reuse that for the next one if it the same size. But it all adds up.
I needed to work out exactly which bits of the code were taking the most time during initialisation. To do this, I used the Shark and Instruments performance tools to get an idea of what code was executing at startup and what exactly was happening. You might think you know your code well, but it’s always good to look at it running to see if something is happening that you didn’t anticipate. To be honest, these tools are very powerful, but they don’t always give you simple answers.
Then, I fell back on a tried and tested debugging technique, the printf. I added code at the beginning of the main() function to take note of the time using CFAbsoluteTimeGetCurrent() and store it in a global variable. Then, I wrote a function that measured the time from that point, and printed out to the console how long things had taken. So I ended up with messages like:
Time for Init: 0.89s
Time for building layouts: 0.91s
Time for loading themes: 0.45s
Time for drawing: 1.43s
Total launch time: 3.68s
It’s worth pointing out that there is very little point in doing performance testing against an app running in the iPhone Simulator – firstly, Macs tend to be way faster, and secondly the performance characteristics are not always the same – some things will be much slower than others on an actual iPhone. Do all your performance testing against your app running on a real device. Also, run the app a couple of times in a row – the first time the app is copied down to the device and run in the debugger it will be a good bit slower at starting up than the second time.
Some devices are faster than others as well. In my testing, an original iPhone took around 4 seconds to launch PCalc, an iPod Touch 2G took around 3 seconds, and an iPhone 3GS took around 2 seconds. If you are doing all your development on a 3GS or a Touch 2G, what feels fast to you might feel a good bit slower to somebody with an original iPhone. I try and do all my performance testing on my original iPhone, if it’s acceptable there, it will be amazing for everybody else.
Also, you want to make sure you are measuring the total startup time including drawing. At the end of your initialisation code when you have built all your views, and you think your app is running, the OS will call the drawRect method on any visible custom views before it displays them.
In the case of PCalc, this was a significant chunk of time, because drawing the LCD, for example, loaded all the images required for the numbers. And the first time I loaded an image, the OS needed to load up the required code to read a PNG file. And so on. What I did for the purposes of testing was work out the last view that was drawn, and put another call to my timing function in the drawRect so I could get the actual point in time where that was finished running.
Now that you have a good handle on where all the time is spent you can do something about it.
Here’s a good tip – if you have a routine that takes only 0.1s to run and it only runs once during startup, there is no point spending hours optimising it. Even if you can make it run twice as fast, you’ll still only be saving a small fraction of a second. Looks for the big ticket items and start with them. Don’t assume that something is slow, measure it first, or you might waste time optimising code that doesn’t need it.
But you might also find that you are doing things during initialisation that you don’t need to do at all. Don’t build things that you don’t need yet, try and do everything only when it’s required. For example, in PCalc, I used to initialise the conversions and constants at startup, which involved reading in all the data from various XML files. It didn’t take too long, but there was still no reason to do it at startup – now, I wait until the user actually presses the conversions or constants buttons, and do it there.
Anyway, after spending a week sitting measuring everything, and optimising code along the way, I managed to shave a whole second off the launch time – three seconds instead of four on my iPhone. It doesn’t seem like a lot of saving for all that work, but it did make a difference. The key is to test everything – do some benchmarking every time you make a change so you can see if your changes are helping or not.
Along the way, I had an another idea. Going back to the Default.png image file, I said some of the Apple applications modify it. What they do is save an image of the application when they quit and save the state of the application, that way, the default image will match the real application when it’s finished loading again. I couldn’t do this with PCalc because third-party apps can’t replace the default image.
But what I could do is keep the splashscreen default image, and then as quickly as possible display my own saved image before I loaded the calculator. So, I moved all the initialisation code out of the initialisation. When PCalc starts up now, all it does now is look for a “Loading.png” file in the documents folder and displays it in a simple image view. If it’s not present, it displays a copy of the splashscreen image. The last thing I do is call -performSelector:afterDelay: to schedule a call of the real initialisation code later.
Then, because my initialisation is over and my app is now running (even though it’s not doing anything other than displaying an image), the OS hides the default image and zooms in the actual application which is showing my loading image. This happens really quickly and gets rid of the splashscreen within a second.
I created the loading image something like this:
UIImage * theLayerImage = NULL;
UIGraphicsBeginImageContext([[UIScreen mainScreen] bounds].size);
theLayerImage = UIGraphicsGetImageFromCurrentImageContext();
NSData *imageData =
[imageData writeToFile:pathToLoadingImage atomically:YES];
So, now I have an accurate, if fake, image of my application being displayed while my real initialisation code is being run. To avoid people thinking it was really running and trying to type on it before it was ready, I decided to display a little loading dialog like so.
As I discovered, the UIActivityIndicatorView animation is run on a thread by the system – that means, even though my application is blocked inside its initialisation code while it starts up, the spinner will still be spinning. This looks great, and gives feedback to the user that something is happening at least. I wrote lots of silly startup messages which are displayed randomly, and got some great suggestions from my testers too.
So, I had got the startup time down to three seconds from four with all my initial optimisations. How long did it take to launch with the new loading screen? Three and a half seconds. Hmm. But it felt a lot faster because you saw the calculator appear almost instantly, and then there was at least some feedback that things were happening under the surface.
So I did some user testing. Everybody I tested it on said that the 3.5 second launch time with the loading screen was faster than the 3 second launch time with just the plain splashscreen, so it was clear that was the way to go.
There was another thing I hadn’t considered. I got complaints from my testers who had an iPhone 3GS or a iPod Touch 2G that they couldn’t read the messages on the loading dialog. They went past too quickly, and it felt annoying. The time it took to do all my initialisation on those devices was less than a second. So what I did was not show the loading dialog and spinner, but still display the fake cached image of the calculator.
Because the time is so short, it looks like the calculator appears instantly, but there a short delay before the application is really running. But most people don’t instantly tap the buttons, so I figured I might get away with it.
Of course, I needed to test what happened if people did tap immediately. And I made another discovery. Because I had started the application event loop running before doing my initialisation, any taps on the screen were queued up correctly. So if you tapped 1, 2, 3 on my loading picture, when the real event loop is finally running I would still get the taps and not actually miss anything.
The end result is that PCalc feels like it loads even faster than it should on a 3GS or a Touch 2G. I experimented with hiding the dialog on slower devices too, but the delay was just a little bit too long before you saw the results of your typing, and the magic spell was broken.
In any case, I think this technique of saving an image on quit and displaying that while your true initialisation happens really helps in making your application feel faster to launch. If your initialisation doesn’t take very long, you can probably skip showing any kind of loading dialog on top of it, even on a slower device.
That’s all for this lesson, I hope it proves helpful!