As we mentioned, a closure closes over the variables and constants it refers from the enclosing context. Now, if the closure is executed synchronously since the caller is blocked waiting for it to return, the closure may safely assume all variables and constants from the enclosing context still exist. On the contrary, when a closure is executed asynchronously, that is, without blocking the caller, there is no such guarantee, and it may happen that the closure is executed when the original enclosing context no longer exists. To ensure, then, that the variables and constants the closure uses outlive the enclosing context, Swift closures are able to capture those variables and constants:
import Dispatch
struct Book {
var title: String
var author: String
var price: Double
}
func sellBook(_ book: Book) -> () -> Double {
var totalSales = 0.0
func sell() -> Double {
totalSales += book.price
return totalSales
}
return sell
}
let bookA = Book(title: "BookA", author: "AuthorA", price: 10.0)
let bookB = Book(title: "BookB", author: "AuthorB", price: 13.0)
let sellBookA = sellBook(bookA)
let sellBookB = sellBook(bookB)
sellBookA() //-- book sales for bookA: 10
sellBookB() //-- book sales for bookA: 13
sellBookA() //-- book sales for bookA: 20
sellBookB() //-- book sales for bookA: 26
When you pass a closure into another function or method, you should pay attention to whether that closure is executed synchronously or asynchronously, and, in the latter case, add the @escaping modifier to the closure parameter declaration. For example, if you define a delay function that is meant to execute a function after some time, that is, after the delay function exits, then Swift will require you to mark the parameter that takes the closure with @escaping:
// If you remove @escaping here, the compiler will complain
func delay(_ d: Double, fn: @escaping () -> ()) {
DispatchQueue.global().asyncAfter(deadline: .now() + d) {
DispatchQueue.main.async {
fn()
}
}
}
For Swift, it is easy to realize the closure may escape the function scope, since you pass it to a dispatch queue. Still, it wants to make sure you know what you are doing and requires you to use @escaping.
If you recall our discussion of automatic reference counting in Chapter 2, Understanding ARC and Memory Management, it is easy to understand how capturing variables and constants works under the hood. In a simplified view of this process, when a closure captures a variable or constant, it automatically increments its reference count, thus ensuring it is not freed when the enclosing scope exits. After the closure is executed, the reference count of all captured entities is decremented, so they can be freed if need be. This makes capturing values in a closure as transparent and automatic a mechanism as possible, with just one exception: preventing retain cycles.
To understand where retain cycles stem from, we may consider two typical scenarios where using a closure may cause a retain cycle:
- An instance O of class C creates a closure, L, and stores it in a member variable of C, so it can be used by other methods of C or from other classes.
- An instance O of class C registers a closure, L, as a KVO observer of one of its strong properties.
In both scenarios, if the closure refers self in its body, then a retain cycle will ensue, since the reference to self from the closure body will make the closure capture self. We can say that the closure holds a strong reference to self.
What happens in both cases is that the closure keeps object O alive by increasing its retain count. At the same time, since O owns the property where the closure is stored, O increases the closure's retain count. Thus, the two entities will indefinitely keep each other alive and your program will end up having a memory leak. Indeed, when O's owner is done with O and frees it, O's retain count is decreased; however, this is not sufficient to free the object O, since the closure L is still retaining it.
So, now we know how retain cycles ensue within closures, your question may be: how can we avoid them? The answer is simple: by breaking the cycle. Let's consider the following code:
class OperationJuggler {
private var operations : [() -> Void] = []
var delay = 1.0
var name = ""
init(name: String) {
self.name = name
}
func addOperation(op: (@escaping ()->Void)) {
self.operations.append { [weak self] () in
if let sself = self {
DispatchQueue.global().asyncAfter(deadline: .now() + sself.delay) {
print("Delay: " + String(sself.delay))
op()
}
}
}
}
func runLastOperation(index: Int) {
self.operations.last!()
}
}
Here, we have an OperationJuggler class that manages a list of operations through an operations array. Operations are just functions or closures that, for the sake of this example, take no arguments and have no return value. This code demonstrates the pattern we have described already: OperationJuggler stores the closure created by addOperation in its operations property. The closure refers self, so it holds a strong reference to it, but the operations array, which is strongly referenced by self, also takes a strong reference to the closure. Unfortunately, the preceding code will compile just fine. Even worse, Xcode Analyze command is not able to detect the cycle. Therefore, you will end up knowing that, at runtime, your app may crash due to its memory footprint.
It is not hard to show that the preceding code indeed creates retain cycles that prevent OperationJuggler instances from being freed when you are done with them. For example, we could use OperationJuggler in a minimal ViewController, like in the following snippet:
class ViewController: UIViewController {
var opJ = OperationJuggler(name: "first")
override func viewDidLoad() {
super.viewDidLoad()
opJ.addOperation {
print("Executing operation 1")
}
self.opJ.runLastOperation(index: 0)
//-- this will release the previous OperationJuggler
self.opJ = OperationJuggler(name: "replacement")
}
}
In viewDidLoad, we added an operation to our OperationJuggler, stored in the opJ property; then, after one second, we overwrote the OperationJuggler in opJ with a new instance. We would expect that the original instance is freed when we do it, but this is not the case. You can easily convince yourself of this by adding a deinit method to OperationJuggler and checking that it is not ever called:
class OperationJuggler {
...
deinit {
print("OperationJuggler freed")
}
}
So, how can we remove this retain cycle? Swift has a special syntax for this that you can use to break the cycle, as shown in the following rewrite of OperationJuggler:
class OperationJuggler {
private var operations : [() -> Void] = []
var delay = 1.0
var name = ""
init(name: String) {
self.name = name
}
func addOperation(op: (@escaping ()->Void)) {
self.operations.append { () in
DispatchQueue.global().asyncAfter(deadline: .now() + self.delay) {
op()
}
}
}
func runLastOperation(index: Int) {
self.operations.last!()
}
deinit {
self.delay = -1.0
print("Juggler named " + self.name + " DEINITIED")
}
}
As you can see, a new [weak self] attribute has made its appearance in the closure body. This will prevent the closure from retaining self, thus not creating the retain cycle. As a consequence of that, it cannot be taken for granted that the closure, to which the self object refers, still exists when it is executed, as it may have been freed. In this case, in accordance with weak ownership, self will be nil. In other words, self is an option type in the closure body, and we need to qualify its use with ?, !, or a guard. In the preceding example, we opted for using the forced unwrapping ! operator, since we are not really interested in preventing our program from crashing, but, in production code, you should be more vigilant. For example, we could write our closure like that shown in the following snippet:
func addOperation(op: (@escaping ()->Void)) {
self.operations.append { [weak self] () in
DispatchQueue.global().asyncAfter(deadline: .now() +
(self?.delay ?? 1.0)) {
op() }
}
}
But more generally, we should prefer a guard:
func addOperation(op: (@escaping ()->Void)) {
self.operations.append { [weak self] () in
guard let self = self else { return }
DispatchQueue.global().asyncAfter(deadline: .now() + self.delay) {
op()
}
}
}
The fact that OperationJuggler manages operations that are closures is just a way to make our example richer, but the retain cycle would ensue just the same if it handled integers, as in the following code:
class IncrementJuggler {
private var incrementedValues : [(Int) -> Int] = []
var baseValue = 100
func addValue(increment: Int) {
self.incrementedValues.append { (increment) -> Int in
return self.baseValue + increment
}
}
func runOperation(index: Int) {
//...
}
}
Admittedly, retain cycles do not ensue only in the two scenarios mentioned here, nor are they always as easy to spot as in our examples. For example, you will also get a retain cycle when you store a closure in an object, O', which is owned by O, as it happens when you register a closure from your view controller with the view model that the view controller owns. As a general rule of thumb, you should pay special attention every time you are using a closure that refers to self, although it would be overkill to handle all such closure as if they would cause a retain cycle, that is, by using [weak self].
Another approach to not creating retain cycles is passing self as an argument into the closure, as shown in the following example. Notice, though, that in this case, the self in the closure body ceases to be a keyword, becoming just a parameter name.
func addOperationAlsoOK(op: (@escaping ()->Void)) {
self.operations.append { (self) in
DispatchQueue.global().asyncAfter(deadline: .now() + self.delay) {
op()
}
}
}
For the code above to compile, though, you will have to modify our definition of self.operations so stored closures accept an argument. This is left as an exercise for the reader.