ARC and Memory Management in Swift
In this tutorial, you’ll learn how ARC works and how to code in Swift for optimal memory management. You’ll learn what reference cycles are, how to use the Xcode 10 visual debugger to discover them when they happen and how to break them using an example of a reference cycle in practice. By Mark Struzinski.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ARC and Memory Management in Swift
30 mins
- Getting Started
- Is That in Scope?
- An Object’s Lifetime
- Reference Cycles
- Checking Your References
- Hold the Phone(s)
- Weak References
- Unowned References
- Who’s Your Carrier?
- Break the Chain
- Reference Cycles With Closures
- Capture Lists
- Capture Your Self
- Using Unowned With Care
- Disarming the Trap
- Now for Something Different
- Finding Reference Cycles in Xcode 10
- Where Is That Leak?
- Bonus: Cycles With Value Types and Reference Types
- Reference and Value
- Where to Go From Here?
Weak References
To break strong reference cycles, you can specify the relationship between reference counted objects as weak
.
Unless otherwise specified, all references are strong and impact reference counts. Weak references, however, don’t increase the reference count of an object.
In other words, weak references don’t participate in the lifecycle management of an object. Additionally, weak references are always declared as optional types. This means when the reference count goes to zero, the reference can automatically be set to nil
.
In the image above, the dashed arrow represents a weak reference. Notice how the reference count of object1
is 1 because variable1
refers to it. The reference count of object2
is 2, because both variable2
and object1
refer to it.
While object2
references object1
, it does so weakly, meaning it doesn’t affect the strong reference count of object1
.
When both variable1
and variable2
go away, object1
will have a reference count of zero and deinit
will run. This removes the strong reference to object2
, which subsequently deinitializes.
Back in the Phone
class, change the owner
declaration to match the following:
weak var owner: User?
This breaks the User
to Phone
reference cycle by making the owner
reference weak.
Build and run again. Now user
and phone
deallocate properly once the runScenario()
method exits scope.
Unowned References
There is another reference modifier you can use that doesn’t increase the reference count: unowned
.
What’s the difference between unowned
and weak
? A weak reference is always optional and automatically becomes nil
when the referenced object goes away.
That’s why you must define weak properties as optional var
types for your code to compile: The property needs to change.
Unowned references, by contrast, are never optional types. If you try to access an unowned property that refers to a deinitialized object, you’ll trigger a runtime error comparable to force unwrapping a nil
optional type.
Time to get some practice using unowned
.
Add a new class CarrierSubscription
at the end of MainViewController.swift:
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
let user: User
init(name: String, countryCode: String, number: String, user: User) {
self.name = name
self.countryCode = countryCode
self.number = number
self.user = user
print("CarrierSubscription \(name) is initialized")
}
deinit {
print("Deallocating CarrierSubscription named: \(name)")
}
}
CarrierSubscription
has four properties:
- Name: Name of the subscription.
- CountryCode: Country of the subscription.
- Number: Phone number.
-
User: Reference to a
User
object.
Who’s Your Carrier?
Next, add the following to User
after the name
property:
var subscriptions: [CarrierSubscription] = []
This adds a subscriptions
property, which holds an array of CarrierSubscription
objects.
Also, add the following to the top of the Phone
class, after the owner
property:
var carrierSubscription: CarrierSubscription?
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
func decommission() {
carrierSubscription = nil
}
This adds an optional CarrierSubscription
property and two new methods to provision and decommission a carrier subscription on the phone.
Next, add the following to init
inside CarrierSubscription
, just before the print
statement:
user.subscriptions.append(self)
This adds CarrierSubscription
to the user’s array of subscriptions.
Finally, add the following to the end of runScenario()
:
let subscription = CarrierSubscription(
name: "TelBel",
countryCode: "0032",
number: "31415926",
user: user)
iPhone.provision(carrierSubscription: subscription)
This creates a CarrierSubscription
for user
and provisions iPhone
with it.
Build and run. Notice the console printout:
User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized
Again, you see a reference cycle: Neither user
, iPhone
or subscription
gets deallocated at the end.
Can you find where the issue is now?
Break the Chain
Either the reference from user
to subscription
or the reference from subscription
to user
should be unowned
to break the cycle. The question is, which of the two to choose. This is where a little bit of knowledge of your domain helps.
A user owns a carrier subscription, but, contrary to what carriers may think, the carrier subscription does not own the user.
Moreover, it doesn’t make sense for a CarrierSubscription
to exist without an owning User
. This is why you declared it as an immutable let
property in the first place.
Since a User
with no CarrierSubscription
can exist, but no CarrierSubscription
can exist without a User
, the user
reference should be unowned.
Change the user
declaration in CarrierSubscription
to the following:
unowned let user: User
user
is now unowned
, breaking the reference cycle and allowing every object to deallocate. Build and run to confirm.
Reference Cycles With Closures
Reference cycles for objects occur when properties reference each other. Like objects, closures are also reference types and can cause cycles. Closures capture, or close over, the objects they operate on.
For example, if you assign a closure to a property of a class, and that closure uses instance properties of that same class, you have a reference cycle. In other words, the object holds a reference to the closure via a stored property. The closure holds a reference to the object via the captured value of self
.
Add the following to CarrierSubscription
, just after the user
property:
lazy var completePhoneNumber: () -> String = {
self.countryCode + " " + self.number
}
This closure computes and returns a complete phone number. The property is lazy
, meaning that you’ll delay its assignment until the first time you use the property.
This is necessary because it’s using self.countryCode
and self.number
, which aren’t available until after the initializer runs.
Add the following line at the end of runScenario()
:
print(subscription.completePhoneNumber())
Accessing completePhoneNumber()
will force the closure to run and assign the property.
Build and run, and you’ll notice that user
and iPhone
deallocate, but CarrierSubscription
does not, due to the strong reference cycle between the object and the closure.
Capture Lists
Swift has a simple, elegant way to break strong reference cycles in closures. You declare a capture list in which you define the relationships between the closure and the objects it captures.
To illustrate how the capture list works, consider the following code:
var x = 5
var y = 5
let someClosure = { [x] in
print("\(x), \(y)")
}
x = 6
y = 6
someClosure() // Prints 5, 6
print("\(x), \(y)") // Prints 6, 6
x
is in the closure capture list, so you copy x
at the definition point of the closure. It’s captured by value.
y
is not in the capture list, and is instead captured by reference. This means that y
will be whatever it is when the closure runs, rather than what it was at the point of capture.
Capture lists come in handy for defining a weak
or unowned
relationship between objects used in a closure. In this case, unowned
is a good fit, since the closure cannot exist if the instance of CarrierSubscription
has gone away.