In this post, I'll explore how Swift's memory management works under the hood, and how the memory modifiers: unowned
and weak
, affect an object's lifetime. You'll get a deeper understanding of how Swift manages objects' lifetime internally.
Memory Management
For example, in C, only the developer is in charge of deallocating unused objects. This can lead to memory leaks, double deallocations, or the use of invalid memory areas.
We don't want this.
Swift uses automatic reference counting (ARC) under the hood to deduce objects' lifetime and automatically deallocate unused objects. Swift has three different types of the reference count. They count how many other instances use an object. And when it is not needed, it is deallocated.
Strong Reference
The counter that is responsible for deallocation is a strong reference counter (RC). The strong RC counts strong references to the object. When the strong RC reaches zero the object is deinited.
A strong reference is just a regular object usage. Creating a variable, or a constant, or saving a reference to an object in another object's property — they all create a strong reference.
Why a developer should even care about reference counting? Seems like a low-level implementation detail that is not important. But actually, it's crucial.
Take a look at this example
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A // Person -> Apartment: strong reference
unit4A!.tenant = john // Apartment -> Person: strong reference
john = nil // Person is no longer needed
unit4A = nil // Apartment is no longer needed
In the above example, the Person
and Apartment
objects have a strong reference to each other, creating a retain cycle. As a result, when you set both john
and unit4A
to nil
, the deinitializers are not called and the objects are not deallocated.
That's where memory management modifiers come in handy.
Weak Reference
One of the solutions to the problem of a retain cycle is a weak reference. It is created using the weak
modifier like that:
let person = Person(name: "John Appleseed") // person is a strong reference
weak var weakPerson = person // weak reference to the same object
Weak var always has an optional type and cannot be constant (let
). That's because the object can be deallocated while it is still referenced by a weak variable. In this case, the variable is automatically set to nil
.
weak
reference like the one that needs an object but can go on correctly without it (using nil
), allowing it to deallocate when nobody else needs itLet's take a look at the solution to the problem above using the weak
modifier:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A // Person -> Apartment: strong reference
unit4A!.tenant = john // Apartment -> Person: weak reference
john = nil
unit4A = nil
Now, retain cycle is no longer here. At first, the Person
object is deallocated because it has no strong references to it. Then, the Apartment
object is deallocated.
No memory leak!
That's it. That is how you break retention cycles in Swift. There is one more modifier that can help you with that.
Unowned Reference
An unowned
reference is very similar to a weak
reference cause it also does not increase a strong reference count. The difference is that it's up to the developer to not use an invalid object.
Unowned variables can be constant or non-optional. When an object is deallocated, ARC does not set the unowned reference’s value to nil
. However, if you try to access a deallocated object, you will catch a runtime error.
Here's a similar example:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
var john: Customer? = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil // No retain cycle, both objects are deallocated
Three Reference Counters
So, how does all this magic works inside? Swift sources have an amazing detailed description of all processes under the hood.
The strong RC counts strong references to the object. When the strong RC reaches zero the object is deinited, unowned reference reads become errors, and weak reference reads become nil. The strong RC is stored as an extra count: when the physical field is 0 the logical value is 1.
The unowned RC counts unowned references to the object. The unowned RC also has an extra +1
on behalf of the strong references; this +1
is decremented after deinit completes. When the unowned RC reaches zero the object's allocation is freed.
The weak RC counts weak references to the object. The weak RC also has an extra +1
on behalf of the unowned references; this +1
is decremented after the object's allocation is freed. When the weak RC reaches zero the object's side table entry is freed.
But what is a side table and why is it needed?
Side Table
An object conceptually has three refcounts. These refcounts are stored either "inline" or in a "side table entry" pointed to by the internal field. You cannot access these fields from Swift directly
class User {
var id: Int
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
let user = User(id: 0, name: "John")
+1
on behalf of strong reference and weak has +1
on behalf of unowned referencesObjects initially start with no side table. They can gain a side table when a weak reference is formed.
Gaining a side table entry is a one-way operation; an object with a side table entry never loses it. This prevents some thread races.
weak var weakUser = user // Side table implicitly created
Strong and unowned variables point at the object. Weak variables point at the object's side table.
This idea is fundamental to understanding how weak
references work. By pointing not to the object but to the side table, the object itself can be deinitialized and fully deallocated.
Weak and Unowned. Deep Differences
Now, by looking at the implementation we can notice important differences between weak
and unowned
.
Performance
Using unowned
introduces less overhead than using weak
. That's because weak
variables reference the object through a side table. This means that there's one more pointer hop to reach the object.
Unowned references point directly to the object, so they do not have such overhead.
Deallocation vs deinitialization
According to the sources, when the strong RC reaches zero the object is deinited. And when the unowned RC reaches zero the object's allocation is freed.
That means that object memory is not available for realocation until all unowned references disappear.
If lack of memory is a problem, consider using
weak
reference because it allows objects to be fully deallocated even when there are alive weak
references.Common Problems
The example of Person
and Apartment
retain cycle can be trivial. It's important to know about common cases when retain cycle appears.
Closures, strong capture, and self
By default, a closure expression captures constants and variables from its surrounding scope with strong references to those values.
As we've already noted, uncontrollable strong references may create a retain cycle. An escaping closure that refers to self
needs special consideration if self
refers to an instance of a class. Capturing self
in an escaping closure makes it easy to accidentally create a strong reference cycle.
For example:
class Person {
var name: String
var voice: Voice? = nil
init(name: String) {
self.name = name
self.voice = Voice {
print("I'm \(self.name)")
}
}
func say() { voice?.say() }
deinit {
print("Person deallocated")
}
}
class Voice {
var say: () -> ()
init(say: @escaping () -> ()) { self.say = say }
deinit {
print("Voice deallocated")
}
}
var person: Person? = Person(name: "Alex")
person!.say()
person = nil
Which outputs only this line — without deinit
prints
My name is Alex
What's going on here? Let's draw a strong references graph:
And, as expected, there is a pretty notable strong reference cycle. The problem is in the creation of the Voice
instance:
self.voice = Voice {
print("My name is \(self.name)")
}
Here, self
is captured with a strong reference to the escaping closure. To solve that, we can capture self
with the weak
modifier:
self.voice = Voice {[weak self] in
guard let self = self else { return; }
print("My name is \(self.name)")
}
With such modification, we receive an expected output:
My name is Alex
Person deallocated
Voice deallocated
weak self
when it is not needed. Remember that strong reference is required so that object is not deallocated before it is needed.Final notes
If you want to achieve an even deeper understanding of ARC internals, definitely check the ARC source code. You can start with this amazing description of an object's lifetime state machine.
Hope that this post was helpful to you. Feel free to leave a comment or to reach me through my social nets!