iOS | ARC & Memory Management in
ARC
ARC works automatically, so you don’t need to participate in reference counting, but you do need to consider relationships between objects to avoid memory leaks. This is an important requirement that is often overlooked by new developers.
An Object’s Lifetime
The lifetime of a Swift object consists of five stages:
- Allocation: Takes memory from a stack or heap.
- Initialization: init code runs.
- Usage.
- Deinitialization: deinit code runs.
- Deallocation: Returns memory to a stack or heap.
Reference counts
, also known as usage counts
, determine when an object is no longer needed. This count indicates how many “things” reference the object. The object is no longer needed when its usage count reaches zero and no clients of the object remain. The object then deinitializes and deallocates.
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.
weak var owner: User?
This breaks the User to Phone reference cycle by making the owner reference weak.
Unowned References
Strong Weak Unownedunowned 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
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
.
Capture Lists
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.
lazy var completePhoneNumber: () -> String = { [unowned self] in
return self.countryCode + " " + self.number
}
This adds [unowned self]
to the capture list for the closure. It means that you’ve captured self as an unowned reference instead of a strong reference.
The syntax used here is actually a shorthand for a longer capture syntax, which introduces a new identifier. Consider the longer form:
var closure = { [unowned newID = self] in
// Use unowned newID here...
}
Here, newID is an unowned copy of self. Outside the closure’s scope, self keeps its original meaning. In the short form, which you used above, you are creating a new self variable, which shadows the existing self variable only during the closure’s scope.
Cycles With Value Types and Reference Types
Swift types are reference types, like classes, or value types, like structures or enumerations. You copy a value type when you pass it, whereas reference types share a single copy of the information they reference.
struct Node { // Error
var payload = 0
var next: Node?
}
Hmm, the compiler’s not happy. A struct value type cannot be recursive or use an instance of itself. Otherwise, a struct of this type would have an infinite size.
class Node {
var payload = 0
var next: Node?
}
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(bert) // Not deallocated
bert.friends.append(ernie) // Not deallocated
}
ernie and bert stay alive by keeping a reference to each other in their friends array, although the array itself is a value type.
Make the friends array unowned and Xcode will show an error: unowned only applies to class types.
// 1
class Unowned<T: AnyObject> {
unowned var value: T
init (_ value: T) {
self.value = value
}
}
// 2
var friends: [Unowned<Person>] = []
// 3
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(Unowned(bert))
bert.friends.append(Unowned(ernie))
}
To access the Person object within Unowned, use the value property, like so:
let firstFriend = bert.friends.first?.value // get ernie