App launching: OperationQueue to the rescue

The challenge: MAD (Massive App Delegate) Modern apps are complex, the iOS community has come up with a few architectural patterns to manage such complexity. Nonetheless, there is a step in the apps lifecycle where this complexity has not yet been tamed: the app launch. It’s quite common to find bugs in which an app […]

The challenge: MAD (Massive App Delegate)

Modern apps are complex, the iOS community has come up with a few architectural patterns to manage such complexity. Nonetheless, there is a step in the apps lifecycle where this complexity has not yet been tamed: the app launch. It’s quite common to find bugs in which an app does not behave as expected, when launched from springboard shortcuts, or notifications. These bugs are usually related to:

  • non-linear navigation, this happens when an app is already running and you try to navigate to a screen that can’t be reached from the current screen
  • issues in managing the request because the app is launching from a cold start.

As developers we want our app to behave correctly and to launch as fast as it can. The iOS watchdog monitors the app’s launch time, and terminates  the app if the startup is not fast enough.

The complexity comes from all the steps that the app needs to perform to be ready to be used. These steps include things like fetching and applying feature flags, executing asynchronous network code, migrating databases, initializing third-party SDKs and other operations such as the handling of universal links, NSUserActivity or quick actions. 

It’s important to note that some of these operations can be executed concurrently, while others should be done in a specific order.

Another source of complexity is time. The AppDelegate is (was, but this is out of topic for this post…) the first object that you see when you create a new project and this usually means that it is one of the oldest files in your codebase. It is supposed to be used as an interface to communicate with the OS, but it ends up being an over complicated and confusing class, in which you can find every kind of code, from network code to UI code. 

Divide et impera

Let’s define what we want to achieve. We want our app launching code to be:

  • Fast
  • Predictable
  • Encapsulated
  • Decoupled
  • Testable
  • Maintainable 

To achieve this we can use the divide et impera strategy. The app setup should be divided into small chunks of initialisation code following the single responsibility principle. These chunks will have dependencies between each other so we need to track which chunk depends on what. Conceptually the app is ready to be used when all these chunks have been executed. An important note is that the code of the chunks must be decoupled, but this does not mean that they can’t be dependent on each other, for example one chunk can produce a result that another chunk will consume as its input. Once we have all these blocks of code we need one or more executors to run them.

Foundation already provides a great way to achieve this: OperationQueue.

Each chunk of code will be an operation. The operations are encapsulated and can be easily tested on their own. Their code is decoupled but it’s very easy to define dependencies between operations in a declarative way and the framework will handle the dependencies for you. Dependencies can be even defined on operations enqueued on different queues running on different threads. 

Defining dependencies in a declarative way will make it very easy to understand and maintain them. It will make your code predictable because you will always know in which order it will be executed.

To make our code faster and to avoid blocking the main thread for too long, we can use multiple queues to execute code in parallel. A possible setup could be:

  • using the main operation queue to execute the operations one by one on the main queue running on the main thread
  • using a background concurrent queue to execute in parallel multiple operations that do not need to be executed on the main queue, such as networking code or database management.

The OS will automatically scale the number of concurrently running operations based on the device resources (RAM, number of CPU cores, etc.) and the dependencies between the operations, which will guarantee the fastest possible execution and the most efficient resource usage.

Show me some code (or pseudo-code)

It’s not easy to provide a real and meaningful example of an app setup, but I want to provide a quick example. A common and complex-enough scenario, is when the user launches the app using a quick action from the home screen. It’s quite easy to handle, isn’t it?

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
            doSomething(shortcut)
        }

        return true
}

Sadly, it is not that easy… because we need to do other stuff first, like initialise the crash reporting sdk.

 func application(_ application: UIApplication,
                  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        AnotherSDK(key: "Th1s1s4S3cretK3y")
        
        if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
            doSomething(shortcut)
        }

        return true
    }

Done! More or less.. Other things to consider include: 

  • The network call which should be fired as soon asthe app starts to fetch the user’s data.
  • The feature toggles should be fetched before performing the action, but after the SDK initialisation. 
 func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        AnotherSDK(key: "Th1s1s4S3cretK3y")
        
        let userDataTask = URLSession.shared.dataTask(with: self.userDataURL) { [unowned self] _, _, _ in
            let featureToggleTask = URLSession.shared.dataTask(with: self.toggleURL) { _, _, _ in
                if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
                    self.doSomething(shortcut)
                }
            }
            featureToggleTask.resume()
        }
        userDataTask.resume()

        return true
    }

How easy is it to understand and change coupled code like this? It already starts to look like a Massive App Delegate.

In the following diagram you can see how the initialisation code can be split into operations and their dependencies (keep a lookout for circular dependencies to avoid potential deadlocks). The blocks represent the operations. The yellow blocks are the initialisations one and the green “handle shortcut” block is the action that the app should perform when the app is ready. The arrows show the dependencies between the blocks.

Now that we know what we want to achieve, let’s see how the code will look. 

// The returned queue executes one operation at a time on the app’s main thread
let mainOperationQueue = OperationQueue.main

let concurrentOperationQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.qualityOfService = .userInitiated
    return queue
}()
    
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let handleQuickAction = BlockOperation { [unowned self] in
        if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
            self.doSomething(shortcut)
        }
    }
        
    let sdkInitializationOperation = BlockOperation {
        AnotherSDK(key: "Th1s1s4S3cretK3y")
    }
        
    let fetchUserDataOperation = AsyncBlockOperation { [unowned self] operation in
        let task = URLSession.shared.dataTask(with: self.userDataURL) { _, _, _ in
            // Do something with the response
            operation.finish()
        }
        task.resume()
    }

    let fetchFeatureToggleOperation = AsyncBlockOperation { [unowned self] operation in
        let task = URLSession.shared.dataTask(with: self.toggleURL) { _, _, _ in
            // Do something with the response
            operation.finish()
        }
        task.resume()
    }
        
        // Setting up the dependencies
    handleQuickAction.addDependency(fetchUserDataOperation)
    handleQuickAction.addDependency(fetchFeatureToggleOperation)
    
    fetchFeatureToggleOperation.addDependency(sdkInitializationOperation)
        
    mainOperationQueue.addOperations([handleQuickAction], waitUntilFinished: false)
    concurrentOperationQueue.addOperations([sdkInitializationOperation,
                                        
    return true
}

The operations can be moved away from the app delegate and from the application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool. The key point here is that each operation is self-contained, they can be tested separately and the dependency management between them is easy to change, it’s declarative and it is decoupled from their creation, making it easy to modify the execution order and the concurrency model.

Note: AsyncBlockOperation is a common Operation subclass that I suggest to add to your codebase. An example can be found here.

Conclusion

In software engineering there are always multiple solutions to address an issue and this is just one of the ways in which you can better handle the application setup.

The most important advantages in this solution are:

  • It is built-in in the iOS SDK. OperationQueue is available since iOS 2.0, it’s been  battle-tested over the years and it’s good to rely on something supported by Apple for a critical part of the app such as the app setup;
  • It is quite easy to implement. Making an operation starting from a block of code is very easy so it should be straightforward to refactor existing code with this approach;
  • The declarative style for dependency management makes it very easy to write predictable code.

On top of all of that, every iOS developer should already be familiar with OperationQueue so it should be easy for everyone in your team to work with them without having to learn another framework or paradigm and, maybe, someone will enjoy this powerful tool even more!

Source: Just Eat