Why Your Lazy Vars Aren’t Creating Strong Reference Cycles in iOS

Even when they capture self

Michael Kiley
7 min readFeb 23, 2021
Photo by freestocks on Unsplash

So you’re fighting the good fight for performant, clean, optimal Swift code, and as a result you’re keeping a weather eye out for sneaky strong reference cycles and the memory leaks they cause. And, as it happens, you have found yourself in a situation that calls for adding a stored property to a class which needs other class members in its computation. This property, however, will not be required immediately and needs some non-trivial amount of time to initialize. Of course, you like to use lazy properties where appropriate for the efficiency improvements they can provide, but you are now in a situation where you will be capturing self in the closure you provide to your lazy var, and this is setting your strong reference cycle alarm a-buzzing! Capturing self in a closure requires an explicit capture list and often requires the use of weak or unowned, right? What precautions do you need to take in this instance? Let’s dive in!

The Background

1. Lazy Properties

A lazy stored property is one whose value won’t be calculated until the first time it’s accessed. This means that it is useful in situations where the value in question requires some non-trivial amount of compute time to create and is not necessarily needed immediately or not always needed. If the lazy property is never accessed, you won’t have to waste that time computing it all, and if it is accessed, at least you won’t have to use up that time on initialization of the class instance storing it. One way of taking advantage of this “laziness” is by setting the value of the lazy property equal to an immediately evaluated closure that computes and returns the value to be used:

A lazy property must always be declared as a var since its value won’t be set at initialization time, and constant let properties require a value before initialization can be considered complete.

2. Closures and Reference Cycles

Since we are using a closure for this lazy property, we had better take a closer look (read: a closure look) at what that means.

A closure is a block of Swift code that can be stored, passed around, and run. Swift functions and methods represent specific cases of closures with extra requirements, but aren’t the only ways closures can be created. Closures can also can be unnamed expressions stored in properties, passed as parameters, or — you guessed it — even used to create lazy properties.

Closures capture values from their context, meaning they store references to these values and allow you to access them within your closure. Most of the time, a value is captured automatically by a closure when you access it from within the closure’s body.

Special care, however, needs to be taken when a closure captures self. Since closures are reference types, and store references to captured values, accessing self from within a closure may put you in danger of a strong reference cycle and the resulting memory leak.

Let’s consider why this is. Memory for Swift reference types is managed via Automatic Reference Counting. This means that Swift keeps track of the number of references to any class instance in your running code, and once that number of references hits 0, it knows it can deallocate the memory given to that instance. If, however, you have two classes that store references to each other, you will never be able to get this count down to 0 for either class, causing the instances and the memory they use up to stay around forever. This is known as a strong reference cycle and causes a memory leak in your program.

Since closures are reference types, storing a closure on a class and then accessing self from within that closure can cause a strong reference cycle because of the references the closure and class would then have to one another. Fortunately, Swift provides a way out of these strong reference cycles via closure capture lists, which specify how captured values should be treated. Within a closure’s capture list, you can specify whether a captured reference should be a normal strong reference, a weak reference, or an unowned reference, as follows:

While a strong captured reference leaves you vulnerable to a reference cycle, weak and unowned references are not counted during automatic reference counting, giving you a way to capture a reference to self without creating a memory leak. The difference between weak and unowned references is that weak references are meant to refer to instances that will have a shorter lifetime than the class storing the reference to them, while unowned references are meant to point to instances that will last the same amount of time or longer than the class pointing to them (more on that can be found in the Swift docs here).

And, just to be safe, Swift forces you to refer to self explicitly when referencing it from within a closure, just to remind you of the strong reference cycle danger.

Getting Closer…

So now we return to our lazy property.

I want to reference self from within the closure I provide to my lazy property. Since the fact that it is a lazy property guarantees that my closure will not be evaluated until after initialization is complete, it is possible for me to reference self from within the closure, since we know that self has been properly initialized by the time the closure is called. I don’t want to create a reference cycle, however, so that might lead me to write something like this:

// Lazy var capturing unowned self
lazy var myLazyVariable = { [unowned self]
return self.someValue * 5
}()

An easy way to test for strong reference cycles is by using a class’s deinit() function, which is called when the class is deinitialized and its memory is freed. Since deinit will never be called when a strong reference cycle is keeping the instance around, I can put a print statement in deinit to verify that my instances are disposed properly. The following playground puts this into practice: First, it defines a class with a lazy property whose closure explicitly captures unowned self. Next, it creates a reference to an instance of that class and accesses the lazy property. Finally, it sets its reference to that class to nil, which should free that class instance up for deallocation. If I have successfully avoided a strong reference cycle, “Person deinit called” should be printed.

Playground with lazy var closure capturing unowned ref to self
Playground output: No reference cycle created

My output shows that capturing self as unowned did, in fact, prevent a strong reference cycle, allowing ARC to clean up my Person instance’s memory.

…The Big Finish

The unexpected occurs, however, when I remove the capture list and the explicit reference to self from my closure:

Lazy prop closure with no capture list and implicit use of self
Output: No compiler error and no strong reference cycle created

According to the Swift docs:

Swift requires you to write self.someProperty or self.someMethod() (rather than just someProperty or someMethod()) whenever you refer to a member of self within a closure

The above code seems to run contrary to this statement! I capture self in my closure by accessing self’s stored properties, but I don’t explicitly refer to self, and I don’t get a compiler error. Furthermore, I capture a strong reference to self, but do not create a strong reference cycle, as shown by the successful calling of deinit(). What’s going on here?

To answer this, allow me to introduce you to Joe Groff, a Senior Swift Compiler Engineer at Apple. The above question was posed to Groff on Twitter, to which he provided the following reply:

Original tweet here

@noescape is a Swift keyword indicating that the closure will not be retained by its owner and will not be called outside of the lifetime of its context; this stands in contrast to @escaping closures, which can be called after their enclosing function returns.

Since @noescape closures will not outlive their context, they do not create strong reference cycles, and the rules regarding captures lists and explicit references to self are no longer required. And, since a lazy property closure is applied immediately upon access to the property, and this access will only happen via an instance of the class it is a member of, lazy properties are made @noescape by default!

TLDR: When writing a lazy property using an immediately applied closure, the closure is automatically @noescape, and you can therefore reference self implicitly and without using a weak or unowned reference.

Resources
Swift docs on Automatic Reference Counting
Swift docs on Escaping Closures
Swift docs on Lazy Properties
This Github discussion on the use of lazy properties

--

--

Michael Kiley

Software developer. Mobile, serverless, voice. Knows some things, curious about all things.