Closures Thumbnail

Closures are a powerful tool in any Swift developer’s toolbox. As a rookie Swift programmer myself that learned by hacking together mobile apps with whatever tools worked to get the final outcome, I’ve been slowly learning the best practices that improve performance and memory usage in an app. In this post, I share the small tweaks anyone can make to improve closure performance in their app.

Motivation

With the frequency that closures are used, small inefficiencies can add up to large performance and memory issues. Not only does this affect the user experience of your app, but it has a huge impact on CO2 emissions as your app scales. For example, if we assume Uber’s usage statistics, every 1MB increase in app size results in CO2 emissions equivalent to 5 round-trip flights from London to LAX! So, if you are using best practices when writing closures, you are not only improving UX, but you are also reducing your app’s environmental impact.

Now that we’ve linked better coding to saving the world, let’s take a simple closure example and introduce some of these best practices. Consider the following closure that calculates the sum of an array of numbers:

let sumClosure = { (numbers: [Int]) -> Int in
    return numbers.reduce(0, +)
}
let sum = sumClosure([1, 2, 3])

This is a straightforward closure with no apparent performance issues at first glance. However, as we scale up our application, even small inefficiencies can compound.

Optimizing Memory Usage

Imagine our sumClosure is part of a larger class and captures self:

class NumberProcessor {
    var numbers: [Int]

    init(numbers: [Int]) {
        self.numbers = numbers
    }

    var sumClosure: () -> Int = {
        return self.numbers.reduce(0, +)
    }
}

The sumClosure now keeps a strong reference to self, preventing self from being deallocated even if all other references to the NumberProcessor instance are released, leading to potential memory leaks. There are two primary ways to mitigate this issue:

Using “weak self”

Using a weak reference allows the object it is referencing to be deallocated if there are no strong references to it. This is useful in situations where the referenced object might become nil at some point during the closure’s lifetime, like deallocating a network request class instance if the object that sent the request is no longer needed (preventing a retain cycle).

class NumberProcessor {
    var numbers: [Int]

    var sumClosure: () -> Int = {
        [weak self] in
        guard let strongSelf = self else { return 0 }
        return strongSelf.numbers.reduce(0, +)
    }
}

In the previous example, we ensure that if the NumberProcessor instance is deallocated, self becomes nil in the closure avoiding costly memory leaks.

Using “unowned self”

If you are certain that the referenced object will be around when the closure is called, using an unowned reference can improve performance and avoid optional checking. However, this comes with an inherent risk if the closure is called after self is deallocated. If you are 100% sure this won’t happen, using “unowned” ensures we don’t have a strong reference to self (preventing retain cycles) and can create more straightforward code:

class NumberProcessor {
    var numbers: [Int]

    var sumClosure: () -> Int = {
        [unowned self] in
        return self.numbers.reduce(0, +)
    }
}

Refining Execution Time

Let’s say our closure’s task becomes computationally expensive:

class NumberProcessor {

    var sumClosure: () -> Int = {
        return self.numbers.map { $0 * 2 }.reduce(0, +)
    }
}

While this closure is perfectly fine for smaller inputs, as size grows, the execution time of the closure increases. This usually results in a slower UI, especially if this closure is executed on the main thread.

Offloading to a Background Thread

To keep the UI responsive during the execution of our closure, we can offload this task to a background thread. This allows the main thread to stay smooth and responsive, handling user interactions in parallel on a separate thread (we assume the use of “weak self” to ensure safety):

class NumberProcessor {
    var numbers: [Int]

    var sumClosure: (@escaping (Int) -> Void) -> Void {
        return { [weak self] completion in
            DispatchQueue.global(qos: .userInitiated).async {
                guard let self = self else { return }
                let result = self.numbers.map { $0 * 2 }.reduce(0, +)
                DispatchQueue.main.async {
                    completion(result)
                }
            }
        }
    }
}

Now our closure takes a completion handler as input, performs the computation on a separate background thread, and once completed, passes the result back to the main thread by the completion handler. Of course, it’s not really revolutionary to suggest multi-threading to improve performance of a mobile application, but it would be remiss to exclude a discussion of background threads when detailing best practices for optimizing closures.

The Final Touch: Lazy Initialization

Now that our closure performs a computationally-intensive task and if we assume it is not always needed immediately after the creation of a NumberProcessor instance, we should consider initializing it lazily. This allows us to reduce the initial load time of the NumberProcessor instance and only allocate resources when performing the computation.

class NumberProcessor {
    var numbers: [Int]

    lazy var sumClosure: ( @escaping (Int) -> Void ) -> Void = { [weak self] completion in
        DispatchQueue.global(qos: .userInitiated).async {
            guard let self = self else { return }
            let result = self.numbers.map { $0 * 2 }.reduce(0, +)
            DispatchQueue.main.async {
                completion(result)
            }
        }
    }
}

Key Takeaways

In summary, these are some of the most common issues I’ve encountered with Swift closures and their solutions:

  • Memory leaks from strong references to self
    • Use weak or unowned references to prevent retain cycles
  • Slower execution times from complex closure operations
    • Use background threads to offload heavy computation
    • Add lazy initialization (when the closure is not needed immediately after creating an instance)

By picking up some of these best practices, you can hopefully make a direct positive impact on the environment and your users’ experience with your Swift app. Thank you for sticking to the end, and if you have any other suggestions or just want to get in touch, feel free to reach out!