iOS App Launch time analysis and optimizations

Avijeet Dutta
Globant
Published in
7 min readAug 27, 2017

The first impression of any product is the most crucial for its long term success. The unboxing experience has to be the best, If that’s true for tangible products like mobile phones and so is true for Mobile Apps as well, which is the heart and soul of those physical devices.

When a user downloads an app from the AppStore all he cares about is: just click on the App Icon from the Home screen and experience everything the app has to offer as quickly as possible.

That being said no matter how fancy the “Launch” screens of the applications look like, maybe through beautiful animations or just mere transitions from the Launch screen through a Splash screen and finally the Welcome/Home screen.

Well, all that might sound obvious, but under the hood, the app developers are hiding their mistakes behind the launch screens without the consent of the Users. And if you’re a developer you must have already realized, what am I talking about. You see, doing so you’re killing the first impression of your Users and in general about your App itself. And not to mention the Users of such Apps might rate the Apps poorly and/or abandon the Apps altogether.

So what should you do…?

First:

Let’s analyze the problem statement, and to begin with we need to first understand the type of App Launches:

  1. Cold Launch: This simply means the App process does not exist in the System’s(OS) kernel buffer cache. Consider the app is launched for the first time or the device has been rebooted and the kernel cache is cleared. This is the worst case for the app launch and hence should be considered for the app launch optimization analysis. This means every time you want to measure your app launch reboot the device.
  2. Warm Launch: This is the type of launch where the application process and data exist in the System’s memory and all that System does is bring the app to the foreground from the existing instance of the app in the memory. To simulate this the app can be just killed and relaunched again.
  3. Hot Launch: This launch means the app is not killed but could be backgrounded or suspended and the app is invoked and brought to the foreground.

These three types of launches are common to both iOS and Android platforms. However, Android the third type of launch is referred to as Lukewarm launch which lies between the Cold and Warm launch types. In Android, this type of launch means that the application process might exist in the memory but the instance of the Activity has to be re-created from scratch. Or if both the process and Activity doesn’t exist in the System’s memory but the recreation of the Task can be benefited from the savedInstanceState bundle, which is the parameter to the onCreate() method of Activity.

Second:

Let’s analyze the type of App Launch timings and see what we can do to make it better.

Coming back to the iOS world; The App launch time can be bifurcated into two:

  1. pre-main() time: This is the time before the UIApplicationMain is returned. Or to simply say, when your Application’s main() method is called and you get the control on the App by the OS. And you already must have realized so far, since this time is not in your control hence difficult to manage. However, dyld has a built-in measurement(An environment variable: DYLD_PRINT_STATISTICS) to analyze this. Which is as depicted in the snapshot below.
Environment variable setting on Xcode to calculate pre-main time of the App

And as you must have already noticed the build configuration should be set to Release for the calculation, because this is the configuration with which the app will be released on AppStore. Also, it’s always recommended to do this exercise on an iPhone instead of a simulator. Once the configuration is done and the app is run, you would see the pre-main() timing details on the console something like this:

Total pre-main time: 1.4 seconds (100.0%)
dylib loading time: 1.1 seconds (77.4%)
rebase/binding time: 242.40 milliseconds (16.6%)
ObjC setup time: 33.04 milliseconds (2.2%)
initializer time: 53.47 milliseconds (3.6%)
slowest intializers :
libSystem.B.dylib : 10.72 milliseconds (0.7%)

This data gives you an abstract idea about what it could be in your app that causes it to launch slowly. To know in-depth about each of these types, I would recommend watching the WWDC 2016 talk on Optimizing App Startup Time.

As mentioned in the talk the major reason for your App slow startup time could be the use of multiple dynamic frameworks. While Apple recommends using only 6 but realistically speaking as your app includes more and more features, you tend to have multiple frameworks. Also, as you must already have been using Swift for your applications. Each application has to ship with the Swift standard libraries.

Swift dylib(s) shipped with each iOS App using Swift

Well, I believe these libs must be already optimized by Apple, but they still do exist. However, this is not in your control until we’ve complete ABI stability for Swift. Starting with Xcode 10.2 and iOS 12.2 the Swift standard libraries will no longer be shipped with the apps. This would reduce the thinned IPA size by ~9MB.
That being said the first and most important thing that you should consider doing is: Check for all your dependencies and if possible merge as many of them possible without breaking the logical boundaries of your application. The second step is to convert the dependencies to Static Frameworks instead of Dynamic Frameworks

Until Xcode 9, it was not possible to ship Swift frameworks as Static frameworks, but fortunately, Xcode 9 beta 4 onwards has ABI Source Compatibility, which gives you the flexibility to ship Swift Frameworks as Static Frameworks as well. All you need to do is just change the Build settings of the Framework target(s) as:

Please note this applies true for both Pure Swift frameworks as well as Frameworks which has both Swift and Objective-C in them.

However, a matter of caution if you’re using Objective-C/Hybrid frameworks as static frameworks and exposing Objective-C classes or Category to the Consumer application, you should ensure to setOTHER_LDFLAGS = “-ObjC” under Build Settings of the Framework(s) and of the Consumer application. An example is depicted in the screenshot below:

Build Setting to load all the members of static archive libraries that implement an Objective-C class or category.

Just by merging the frameworks and converting them to Static Frameworks, you would see a great difference before and after for the pre-main() launch time of the application. A simple illustration is shown below with 4 Framework dependencies:

Dynamic vs Static Frameworks impact on pre-main() time

While this should help you win90% of the battle related to the pre-main() launch time of the app, the rest is related to coding best practices as illustrated in the WWDC talk: Optimizing App Startup Time

Not only does static frameworks improve the app loading time, but they are also beneficial for the overall size of the app as well.

With iOS 13, dyld3 will be included in iOS apps as well and this is a big improvement as this will make the iOS apps launch 2x faster. This is a pretty good video from WWDC 2019 that explains all the optimizations in greater detail.

Moving on to next…

2. post-main() time: This factor is something that you as a developer will have full control. Hence, this is very deterministic and easy to understand and manage. This starts through application:willFinishLaunchingWithOptions: and application:didFinishLaunchingWithOptions of your AppDelegate all the way up to the ViewController(s) viewDidLoad andviewWillAppear methods is when your User can see the “Welcome/Home” screen of your App.

Needless to mention, if you’ve got a hierarchy for your AppDelegates and the initial ViewController, you would need to measure the time at all the levels combined at the methods mentioned above. You would always be tempted to put all your global initialization code in the AppDelegate’s lifecycle methods stated above; mostly in the application:didFinishLaunchingWithOptions method. But you must realize that doing so would add more to your App launch time.

A thorough analysis should be done to do the bare minimum work possible during App Launch. And especially in this case, I would recommend making use of three different strategies internally in your AppDelegate’s application:didFinishLaunchingWithOptions method:

  1. First Run Loop: This is a must-have for your app to configure during the first CFRunloop cycle. Ideally, this should be very minimum ~10% of the total work, you want to do during the App launch.
  2. Second Run Loop: This is just a dispatch async on the main queue, but are the stuff that can be deferred to the second CFRunLoop cycle. This should also be as minimal as possible. And
  3. Operations in the background: This is dispatch async on the global queue with thread priority as: DISPATCH_QUEUE_PRIORITY_BACKGROUND Ideally, this is your best bet. Try to defer as much non-trivial app launch work here as possible. Operations such as registration to Analytics, configuring components that aren’t immediately necessary during app launch time, etc. This will certainly gain you better benefits.

Finally:

The simple equation comes out to be:

pre-main() time + post-main() time = Total time of App Launch

That being said, all of the above mentioned strategies should be religiously check listed for every release of your app in order to have a better experience for the Users and hence maximum retention of your product.

--

--