Improving Your iOS App’s Launch Time

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:

  1. 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.
  2. 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’s application:willFinishLaunchingWithOptions: and application:didFinishLaunchingWithOptions: methods. As soon as didFinishLaunchingWithOptions 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 use dispatch_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.


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 after application: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.”

shared_ptr_nonnull and the Zen of reducing assumptions

(This article assumes some familiarity with shared_ptrs in C++.)

Imagine the following line of code and comment are in the private area of the definition of a C++ class Foo:

// The current Quaffle, always valid
shared_ptr<Quaffle> currentQuaffle;

Can you spot any dangerous thinking here? If not, that’s okay, but hopefully this article will change that.

Within the implementation of Foo, because currentQuaffle is assumed to be “always valid,” code dereferences it and uses the Quaffle it points to without checking for validity, i.e.:


rather than

if (currentQuaffle) {

The bug introduced by this assumption was caused when an empty currentQuaffle crashed trying to DoTheThing(). A value had never been set after currentQuaffle was silently created using the shared_ptr default constructor.

It’s easy to imagine other ways a bug could be introduced here. Some other object might pass an empty shared_ptr into an instance of Foo without realizing it, maybe across many layers of the call stack. Or a future developer might call reset() on currentQuaffle inside Foo’s implementation without knowing it’s meant to always be valid. In all these cases, currentQuaffle ends up breaking an unenforced law that it should always be valid.

What’s the solution? Ideally, we could simplify the ownership of currentQuaffle so that Foo has a plain Quaffle rather than use a pointer at all. But if this isn’t feasible, we can still let the type of currentQuaffle encode the rule about validity rather than hoping that developers obey it. Enter shared_ptr_nonnull, a class invented to solve this problem. It’s just like a shared_ptr, except:

  • It lacks functions that would make it empty, i.e. a default constructor and reset() with no arguments, and
  • It fails an assert whenever it’s made empty, like when it’s constructed from an empty shared_ptr. (“Fails an assert” means it traps to the debugger in debug builds. This could arguably be an even stronger failure, but I’ll leave that topic alone for now.)

This class catches bugs at compile time, most often when something tries to default-construct a shared_ptr_nonnull member variable, and at runtime, when someone makes it empty in other ways.

In a nutshell, we were assuming currentQuaffle was always valid, and shared_ptr_nonnull gives us a way to make sure that’s true. I’ve seen this concept come up again and again in software development, across programming, debugging, testing, planning and more. Two of the most important questions you can ask yourself are “What am I assuming right now?” and “How can I make sure it’s true?”

The answer to “How can I make sure it’s true?” might be to write another unit test, to step through with the debugger in a slightly different context, to try a different manual test case, to do some user validation, or a whole host of other options. In this case, the solution was to write a new class. But it took a bug to make us write that class. Preferably, we would have seen that innocuous little phrase “always valid” as an alarm bell going off before we ran into a bug. It takes a particular kind of thinking to see past our own assumptions and we should push ourselves to think in that way as much as possible.

I used the term Zen in the title of this article not because I want you to write code in the lotus position but because Zen meditation focuses on a heightened awareness of things so innate you might never otherwise notice them, like your thoughts and your breathing. If we can train ourselves to be aware of our own innate assumptions, we can write more enlightened code. Thanks for reading, and good luck!