This is a guide to improving the launch time of any iOS app. It covers how to analyze your launch time and some strategies we’ve used here at iZotope to make the Spire app launch faster.
First, measure your launch time
As with any optimization, it’s important to profile first so you know for sure where your app is spending its time. This lets you focus on the slowest areas, and gives you a benchmark so you can tell if you’re actually making progress.
Before you start, you should remember (as pointed out at 24:44 in this great Apple video) that your app launches much more quickly if the app and its data are still sitting in memory in the kernel (warm launch.) This means that if you want to focus on the worst case, you should measure your app’s launch time immediately after rebooting the iOS device (cold launch), rather than just relaunching your app.
Launch stages
In most apps, the total launch time that a user experiences, from tapping your app’s icon to actually using the app, includes two major stages:
- Pre-main time, which is everything that happens before your application’s entry point in
main()
. This includes loading dynamic libraries, rebasing and binding symbols, Objective-C runtime setup, and running Objective-C+load
methods. - Actual launch code, which includes a lot of Apple code that runs down in
UIApplicationMain()
, but also everything that you do in your app delegate’sapplication:willFinishLaunchingWithOptions:
andapplication:didFinishLaunchingWithOptions:
methods. As soon asdidFinishLaunchingWithOptions
returns, unless you block the main thread with other code, your app’s UI is visible and the user can start using the app.
Measuring pre-main time
To measure pre-main time, edit your scheme and add the DYLD_PRINT_STATISTICS
environment variable with a value of 1. When you run your app, the first thing you’ll see in the logs is something like this:
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 888.01 milliseconds (68.9%)
rebase/binding time: 108.64 milliseconds (8.4%)
ObjC setup time: 76.06 milliseconds (5.9%)
initializer time: 215.06 milliseconds (16.6%)
slowest intializers [sic] :
libSystem.B.dylib : 9.35 milliseconds (0.7%)
libMainThreadChecker.dylib : 31.34 milliseconds (2.4%)
AFNetworking : 52.25 milliseconds (4.0%)
YourApp : 69.85 milliseconds (5.4%)
Measuring launch code time
There are a number of ways to measure your application’s launch code time, but my favorite is the Time Profiler in Instruments. In Xcode, choose Product > Profile, select Time Profiler, and start recording. Stop as soon as you see your app enter the Foreground – Active state in the Life Cycle lane, and then select the entire time range. Now you can interact with the call tree and see the time that your app spent in each method or function call. Clicking the Call Tree button in the bottom toolbar gives you some useful options, including the most important one for our purposes here: Hide System Libraries, which lets us focus only on the code that we can actually change. (This user guide has detailed instructions on navigating Instruments.)
Chances are, if you have a slow launch, your app delegate methods are taking a good chunk of the total time, and you can unfold them to see the heaviest calls that they’re making.
Then improve it
After you have a good idea of where your app is spending its time during launch, try some of the strategies listed here to make it faster. This is nowhere near an exhaustive list, but these are the major techniques that we’ve used successfully on our app.
Improving pre-main time
- Load fewer dynamic libraries. This can be one of the longest parts of an app’s total launch time. Apple recommends using only up to six non-system frameworks, but for most modern apps, including ours, this is unrealistic. See if you can remove any of the dynamic libraries you’re using by replacing them with static versions or compiling their sources directly. If that isn’t feasible, you still have a few options:
- We use CocoaPods, and were able to significantly improve our launch time by using a plugin called Amimono, which copies symbols from our pods into the main app executable.
- If you’re using Carthage or managing dynamic libraries manually, you could try a strategy like this one described by Eric Horacek: convert individual libraries into static frameworks, and then link them in the app through a single dynamic framework. I haven’t tried this personally so I can’t guarantee it’s a good idea.
- Use
+initialize
instead of+load
for initialization of Objective-C classes. This defers the code in question until the first message is actually sent to the class, rather than running it during the initial Objective-C runtime configuration. Note that you need to be careful since+initialize
will be called by subclasses. To solve that, you can check the calling class as described here, or usedispatch_once()
for one-off initialization. In our case, we now use+initialize
to create shared objects like dispatch queues with calls like the following:
+ (void)initialize { static dispatch_once_t token; dispatch_once(&token, ^{ queue = dispatch_queue_create("name", DISPATCH_QUEUE_SERIAL); }); }
- Kill dead Objective-C code. Every class you’ve compiled into your app slows down rebase/binding and Objective-C initialization time, so get rid of the ones you aren’t using anymore.
Improving launch code
- Be lazy. If you’re doing something during launch that can happen later, do it later. A simple example is waiting to create a particular view controller until the user actually taps a button to present it. Swift’s lazy properties make this extremely easy in some cases:
private lazy var settingsViewController: MySettingsViewController = { return MySettingsViewController() }() private func onSettingsButtonPressed() { present(self.settingsViewController, animated: true) }
Tiny delays that the user would never notice on a button press add up when there are several happening together during launch. Don’t take this too far, though: if doing something lazily will add a perceptible delay (100 milliseconds, say*) on a user action, it’s better to pay that price up front so that your app feels responsive when the user is interacting with it.
- Use background threads. If something doesn’t need to happen on the main thread, offload it onto a background thread so the main thread can forge ahead with showing your UI. Be careful, because a lot of code is not thread safe or is designed to run only on the main thread. But in some cases, this can be as easy as:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ /* do something */ });
- Compromise on behavior. At the end of the day, the more that your app is trying to do when it launches, the more slowly it will launch. Negotiate with your team to see if there’s anything user-facing that you’re willing to change or remove to get to a faster launch time. Maybe it’s worth more to your users to have a simple initial UI that loads quickly, rather than one that’s full of smooth gradients, drop shadows, and helpful data, but takes forever to show up.
Sleight-of-hand
This post is focused on making your app actually launch faster, but there’s also a lot of psychological trickery you can do to make it feel like it launches faster. I won’t spend a huge amount of time on this, but here are a couple of things that helped us on that front:
- Instead of using an in-your-face splash screen with a giant logo, which draws attention to the fact that your app is slow to launch, use an empty version of your app’s UI as recommended by Apple’s Human Interface Guidelines. This can make your app feel both faster and more professional, since this is what Apple’s apps and almost all top apps on the app store do.
- Try showing a temporary UI with a spinner (
UIActivityIndicatorView
) while you do any initialization that can happen afterapplication:didFinishLaunchingWithOptions:
but must happen before the user can use your app. Unlike other UI elements,UIActivityIndicatorView
animates on its own thread, so you can still block the main thread with your initialization code during this time.
I hope this post is a helpful starting point. Good luck, and have fun!
* It’s hard to make a general rule for how long a delay can be without being perceived, since it’s dependent on the stimulus and the context. But many sources seem to have settled on or around 100 milliseconds. For example, see this 1968 essay by Robert B. Miller, p. 271: “the delay between depressing the key and the visual feedback should be no more than 0.1 to 0.2 seconds.”