Unowned
This post is a followup to An Exhaustive Look At Memory Management in Swift.
I have been asked when it is appropriate to use unowned
versus weak
and I think the answer is a little more complicated than providing best practice scenarios. In fact, this currently seems to be a hot debate in the Swift community.
I’ll provide several examples that demonstrate where unowned
can be used and when it crashes when you least expect it. My goal is for the reader to gain a better understanding of the tradeoffs of using unowned
and hopefully come away with a more informed opinion.
The following code examples can be found on Github.
Rules of Unowned
Before we explore the rules of unowned
references, we’ll look at potential use cases and explore them in more detail throughout this post.
Apple has this to say about unowned
capture semantics in closures:
Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.
Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future.
If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.
Straight from the horses mouth.
unowned
is recommended over weak
when the instance owns the unowned
reference. When you are writing your own code for your application, you are in control of your own destiny. So while unowned
might be safe under your defined constraints, a future developer might unknowingly introduce a runtime crash.
Parent-Child Hierarchy
The example for using unowned
that Apple provides in the Swift documentation is within a parent-child relationship. The parent maintains a strong ownership of the child while the child keeps an unowned
reference to the parent. As long as the relationship dependencies are one-to-one for their lifetime, this is a safe use of unowned
.
class Country {
let name: String
private var territories: [Territory] = []
init(name: String) {
self.name = name
}
func addTerritory(name: String) {
let territory = Territory(name: name, country: self)
territories.append(territory)
}
func printTerritories() {
print(territories)
}
}
class Territory: CustomStringConvertible {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
var description: String {
return "Territory(name: \(name), origin: \(country.name))"
}
}
In the above example, a Territory
is initialized with the object that the unowned
variable references, its Country
of origin. unowned
is more appealing in this example since the territory’s country is immutable and non-optional, indicating that the country cannot change and is a necessary attribute of a territory.
Using weak
would communicate the wrong intent to a fellow developer. The Country
would become mutable and optional. Without documentation, someone might assume the country can or should be nil
at any moment or change for any reason.
Delegates
It might seem that a delegate should be unowned
if the lifetime of the delegate and the listener is expected to be one-to-one. While this can be safe, it’s simple to create a scenario that exposes problems with using unowned
delegates. (Shout out to Peter Livesey for giving me this example!)
protocol Delegate: class {
func foo()
}
class A: Delegate {
var b: B?
func foo() {}
}
class B {
private unowned let delegate: Delegate
init(delegate: Delegate) {
self.delegate = delegate
}
func callFoo() {
delegate.foo()
}
}
At a glance, nothing jumps out as a serious danger. The problem with this code lies exclusively on external consumers of these objects. Let’s examine what happens if someone invokes callFoo()
in a background thread or at the end of the current runloop after the A
instance is deallocated. In the following test, we would expect the code to not crash as our assertion.
func test() {
let a = A()
let b = B(delegate: a)
a.b = b
DispatchQueue.main.async {
b.callFoo()
}
}
If you run this example, your app will trigger a SIGABRT
at runtime with the following error Fatal error: Attempted to read an unowned reference but object 0xabcdef012345 was already deallocated.
The DispatchQueue.main.async
might sound contrived, but it’s not completely out of the question to imagine invoking callFoo
at the completion of a UIView.animation
block or after a URLSession
dataTask
that has been kicked off. And if the listener is a UIViewController
object, then it’s certainly easy to dealloc during the backgrounded delegate execution by navigating back a screen.
While it might defeat the purpose, one solution to the crash is to pass b
in a capture list as weak
, but it’s probably best to make the delegate weak
and live with the question marks.
DispatchQueue.main.async { [weak b] in
b?.callFoo()
}
Since B
did not create the A
instance, it does not technically own the instance and breaks our guidelines for unowned
. If you are the author of this code, you are in control of your own destiny so you may choose to use unowned
in this situation despite the possibility of future misuse.
asdfasfdjhasfjlasjflak;sdjfkl;ajsfl; // TODO! This example also demonstrates external delegates, which might be more common in a third party library.
Lazy
Swift’s Automatic Reference Counting Document details a few examples of where unowned
can be a useful addition. In the example below, the asHTML
closure property would normally capture self
strongly since the body of the closure refers to self
thus capturing the HTMLElement
instance. Marking asHTML
as a lazy
property means that you can refer to self
within the closure, since the property will not be accessed until after initialization and self
is gauranteed to exist.
The fix, as shown, is to use the [unowned self]
capture list to avoid the strong reference cycle.
class HTMLElement {
private let name: String
private let text: String?
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
lazy var asHTML: () -> String = { [unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
}
Closures
In WWDC 2014, Session 403, Apple presented a demo of using unowned
in a closure capture list. Let’s take a look at that example.
class TempNotifier {
var onChange: (Int) -> Void = { _ in }
var currentTemp = 72
init() {
onChange = { [unowned self] temp in
self.currentTemp = temp
}
}
}
The lifetime of the onChange
closure exists as long as its owning object, so in theory there should not be any issues with this code. Let’s verify with a test.
func test() {
let notifier = TempNotifier()
DispatchQueue.main.async {
notifier.onChange(70)
}
}
If we let this code run, it turns out that this does not crash. Our assumption was correct and we were able to execute a test that verified our assumption.
Passing captured unowned
objects in closures isn’t always this easy. Let’s look at a more common example.
class ServiceLayer {
private var task: URLSessionDataTask?
func fetchData(from url: URL, completion: @escaping (Data?) -> ()) {
task = URLSession.shared.dataTask(with: url, completionHandler: { data, _, _ in
completion(data)
})
task?.resume()
}
func cancel() {
task?.cancel()
}
}
class AppleService {
private let service = ServiceLayer()
private var data: Data?
func fetchAppleDotCom() {
service.fetchData(from: URL(string: "https://www.apple.com")!, completion: { [unowned self] data in
self.data = data
})
}
deinit {
service.cancel()
}
}
In our naive ServiceLayer
, the completionHandler
is called with the data
returned from the response or nil
if the request failed. We learned from the previous blog post that URLSession
retains the completion block until after the request has executed. While our heart’s in the right place, we have added code to cancel in flight network requests, and have chosen to use unowned
to set data
to the response of the service call. This might look safe since the service
and the completion
appear to have a one-to-one relationship with another, but we’ll quickly learn that’s not the case.
func test() {
autoreleasepool {
let object = AppleService()
object.fetchAppleDotCom()
}
}
If we run this code we’ll observe a runtime crash Fatal error: Attempted to read an unowned reference but object 0x7fe353e2d580 was already deallocated. If we debug the issue, we’ll learn that our deinit
does get executed on the AppleService
instance, thus cancelling the request. What we’ll also learn is that URLSession
’s completionHandler
is also called when a request is explicitly cancelled. Unfortunately, since the completionHandler
is temporarily retained by URLSession
, it is called after our service instance is deallocated and therefore references an unowned
object that has been deinitialized.
Using unowned
like this is a big assumption. Especially if the service layer is closed source or does not document how its completionHandler
is retained.
Wrapping It All Up
unowned
has a greater chance for success when the unowned
reference is owned by its creator. At the very least, it’s possible to write unit tests to add confidence that unowned
is the right thing to do if you choose to go that route. But if performance isn’t a concern, there’s nothing wrong with sticking with weak
.