Optimizing launch time of modularized iOS app
By: Yusuke Kita
Tags: Optimization, Mobile, Swift, Framework, iOS

Hi, I’m @kitasuke, iOS Engineer.

I would like to share how I optimized app launch time by about 30% using Static Framework. This approach doesn’t guarantee that you get same result because it’s totally depends on what frameworks you use.

Here is a repository of sample project using this approach. Please see for more details below.

Target

Large code base is suitable for the approach and it’s expected to use CocoaPods as a dependency manager with it. Modularized app means both multiple Xcode projects in one workspace and multiple targets in one Xcode project.

Overview

Dynamic Framework is commonly used for Swift. However, it might eventually slow down app launch time. Especially, it matters a lot when you use many frameworks in multiple modules. On the other hand, Static Framework doesn’t take that much app launch time. However, you get a linker error when you use same framework in different modules due to duplicated symbols.

To solve the issues above, I would like to introduce the idea of Umbrella Framework.

Umbrella Framework

The definition of umbrella framework is below.

An umbrella framework is a framework bundle that contains other frameworks.

Adding umbrella framework as dynamic framework that contains common static frameworks for multiple modules enables you to utilize the benefits of static framework as much as possible, but avoid duplicated symbols as a same time.

Below image is a dependency graph of an example app. This app uses RxSwift for reactive programming in multiple modules and Protocol Buffers for definition of data structure. So the umbrella framework contains these frameworks and link it to AppCore which provides core functionality and AppEntity which provides data structures and link them into main target App.

One thing you should keep in mind is that the umbrella framework is not supported on iOS. You can build the umbrella framework if you manually set correct framework paths. However, you’ll see validation errors when you archive or upload your app. I’ll explain how I actually achieved same thing in this post.

Approach

This app uses CocoaPods as dependency manager because it can handle framework path setting smoothly and generate static framework. Additional framework settings will be handled manually.

I strongly recommend to use xcconfig files to manage build settings, but I don’t explain how to use since it’s out of scope.

App structure

Below is project structure of the app.

// UmbrellaApp.xcworkspace

- UmbrellaApp // main target

- UmbrellaCore // core functionality like API client

- UmbrellaEntity // generated model by protobuf

- UmbrellaFramework

- Pods

Below is a Podfile for CocoaPods. There is nothing to do for UmbrellaCore and UmbrellaEntity since UmbrellaFramework is the only framework they need.

source "https://cdn.cocoapods.org/"

platform :ios, '13.0'

use_frameworks!



workspace 'UmbrellaApp'

target 'UmbrellaApp' do



pod 'Firebase/Analytics'

pod 'Firebase/Crashlytics'

pod 'Firebase/Messaging'

end



target 'UmbrellaFramework' do

project 'UmbrellaFramework/UmbrellaFramework'



pod 'RxSwift'

pod 'RxRelay'

pod 'RxCocoa'

pod 'SwiftGRPC'

end

Static Framework

Linkage Customization of use_frameworks! can specify which type of framework it generates. Simple set static for the option.

use_frameworks! :linkage => :static

UmbrellaFramework

Framework paths will be handled by CocoaPods, but that’s not enough.

First, let’s add export.swift file to export symbols of each frameworks.

@_exported import RxCocoa

@_exported import RxRelay

@_exported import RxSwift

@_exported import SwiftGRPC

@_exported import SwiftProtobuf

@_exported lets you export a symbol from another module as if it were from your module.

Next, add -all_load to Other Linker Flags because this app uses RxSwift. This flag explicitly loads all object files and you’ll see a runtime error without it when you run the app with static framework of RxSwift.

ld: warning: Could not find or use auto-linked library 'RxSwift'

ld: warning: Could not find or use auto-linked library 'RxCocoa'

ld: warning: Could not find or use auto-linked library 'RxRelay'

It might be better to add the flag anyway to avoid the issue above.

UmbrellaCore & UmbrellaEntity

Next, let’s set up the umbrella framework in both modules.

As explained, ideal umbrella framework, in other words embedded framework in embedded framework is not supported. So add the umbrella framework with Do Not Embed instead.

Framework paths should be set manually when you add any frameworks with Do Not Embed. Add ${BUILT_PRODUCTS_DIR}/UmbrellaFramework.framework to Framework Search Paths and ${BUILT_PRODUCTS_DIR}/UmbrellaFramework.framework/Headers to Header Search Paths.

Create export.swift here as well for same reason.

@_exported import UmbrellaFramework

This app links both UmbrellaCore and UmbrellaEntity to main target, so make them static framework by setting Static Library to Mach-O Type.

UmbrellaApp

Finally, let’s embed the umbrella framework to main target, UmbrellaApp instead since it’s can not be embedded into embedded frameworks. There is nothing to do for framework path when you embed frameworks.

UmbrellaTestsFramework

If there is a testing framework that depends on one of frameworks in the umbrella framework, you should have both of them in same Xcode project file. Otherwise, you’ll see a runtime error like below.

failed to demangle superclass of xxx from mangled name 'xxx'

To fix this issue, create a new framework, UmbrellaTestsFramework and add RxTest into it.

target 'UmbrellaFramework' do

project 'UmbrellaFramework/UmbrellaFramework'



pod 'RxSwift'

pod 'RxRelay'

pod 'RxCocoa'

pod 'SwiftGRPC'



target 'UmbrellaTestsFramework' do

inherit! :search_paths

pod 'RxTest'

end

end

Make sure that UmbrellaTestsFramework shouldn’t be added to UmbrellaAppTests due to duplicated symbols. So add only RxTest into Link Binaries With Libraries and set ${PODS_CONFIGURATION_BUILD_DIR}/RxTest to Framework Search Paths and ${PODS_CONFIGURATION_BUILD_DIR}/RxTest/Headers to Header Search Paths. I'm not sure if this is correct way, but nothing came to my mind better than this at this moment.

Measuring app launch time

Setting environment variable DYLD_PRINT_STATISTICS enables you to measure statistics of app launch time. Set Name and Value to Environment Variables so that you’ll see the result when you run the app.

Summary

In general, more dynamic framework you share in multiple modules, longer your app takes time to be launched. This approach is not a silver bullet and the result is depends on app structure. However, this optimizes app launch time of your app for sure. If you’re struggling with this optimizations, it’s worth giving it a try.