Memory Management in Swift iOS 25.10.2019

Swift memory management is handled automatically, and you will usually be unaware of it. Objects come into existence when they are instantiated and go out of existence as soon as they are no longer needed. Nevertheless, there are some memory management issues of which even a Swift user must be conscious.

Structs and classes are very different when it comes to lifecycle management. Structs are much simpler in this regard, because they can’t have multiple owners; their lifetime is tied to the lifetime of the variable containing the struct. When the variable goes out of scope, its memory is freed and the struct disappears.

In contrast, an instance of a class can be referenced by multiple owners, which requires a more sophisticated memory management model. Swift uses Automatic Reference Counting (ARC) to keep track of the number of references to a particular instance. When the reference count goes down to zero (because all variables holding a reference have gone out of scope or have been set to nil), the Swift runtime calls the object’s deinit and frees the memory. Therefore, classes can be used to model shared entities that perform cleanup work when they’re eventually freed, like file handles (which have to close their underlying file descriptor at some point), or view controllers (which can require all kinds of cleanup work, e.g. unregistering observers).

Memory management of reference type objects is quite tricky under the hood. Swift normally does all the work for you, but trouble can arise when two class instances have references to one another. When that’s the case, you can have a retain cycle which will result in a memory leak, meaning that the two instances never go out of existence. Some computer languages solve this sort of problem with a periodic "garbage collection" phase that detects retain cycles and cleans them up, but Swift doesn’t do that.

Whenever we create a new instance of a class, ARC allocates the memory needed to store that class. This ensures that there is enough memory to store the information associated with that instance of the class, and also locks the memory so that nothing overwrites it. When the instance of the class is no longer needed, ARC will release the memory allocated for the class so that it can be used for other purposes. This ensures that we are not tying up memory that is no longer needed.

First, I’ll make two class instances and watch them go out of existence:

func testRetainCycle() {
    class Dog {
        deinit {
            print("farewell from Dog")
        } 
    }
    class Cat {
        deinit {
            print("farewell from Cat")
        }
    }
    let d = Dog()
    let c = Cat()
}
testRetainCycle() 
// farewell from Cat, farewell from Dog

When we run that code, both "farewell" messages appear in the console. We created a Dog instance and a Cat instance, but the only references to them are automatic (local) variables inside the testRetainCycle function. When execution of that function’s body comes to an end, all automatic variables are destroyed; that is what it means to be an automatic variable. There are no other references to our Dog and Cat instances that might make them persist, and so they are destroyed in good order.

Now I’ll change that code by giving the Dog and Cat objects references to each other:

func testRetainCycle() {
    class Dog {
        var cat : Cat?
        deinit {
            print("farewell from Dog")
        }
    }
    class Cat {
        var dog : Dog?
        deinit {
            print("farewell from Cat")
        }
    }
    let d = Dog()
    let c = Cat()
    d.cat = c 
    c.dog = d
    // create a retain cycle
}
testRetainCycle() 
// nothing in console

When we run that code, neither "farewell" message appears in the console. The Dog and Cat objects have references to one another. Those are strong references (also called persisting references). A strong reference sees to it that as long as our Dog has a reference to a particular Cat, that Cat will not be destroyed, and vice versa. That’s a good thing, and is a fundamental principle of sensible memory management. The bad thing is that the Dog and the Cat have strong references to one another. That’s a retain cycle! Neither the Dog instance nor the Cat instance can be destroyed, because neither of them can "go first". The Dog can’t be destroyed first because the Cat has a strong reference to it, and the Cat can’t be destroyed first because the Dog has a strong reference to it.

These objects are now leaking. Our code is over; both d and c are gone. There are no further references to either of these objects; neither object can ever be referred to again. No code can mention them; no code can reach them. But they live on, floating, useless, and taking up memory.

Weak references

To break the reference cycle, we need to make one of the references weak or unowned. Assigning an object to a weak variable doesn’t change its reference count. Weak references in Swift are always zeroing: the variable will automatically be set to nil once the referred object gets deallocated — this is why weak references must always be optionals.

So, one solution to a retain cycle is to mark the problematic reference as weak. This means that the reference is not a strong reference. It is a weak reference. The object referred to can now go out of existence even while the referrer continues to exist. Of course, this might present a danger, because now the object referred to may be destroyed behind the referrer’s back. But Swift has a solution for that, too: only an Optional reference can be marked as weak. That way, if the object referred to is destroyed behind the referrer’s back, the referrer will see something coherent, namely nil. Also, the reference must be a var reference, precisely because it can change spontaneously to nil.

This code thus breaks the retain cycle and prevents the memory leak:

func testRetainCycle() {
    class Dog {
        weak var cat : Cat?
        deinit {
            print("farewell from Dog")        
        }
    }
    class Cat {
        weak var dog : Dog?
        deinit {
            print("farewell from Cat")
        }
    }
    let d = Dog() 
    let c = Cat() 
    d.cat = c 
    c.dog = d
}
testRetainCycle() 
// farewell from Cat, farewell from Dog

To break the retain cycle, there’s no need to make both Dog’s cat and Cat’s dog weak references; making just one of the two a weak reference is sufficient to break the cycle. That, in fact, is the usual solution when a retain cycle threatens. One of the pair will typically be more of an "owner" than the other; the one that is not the "owner" will have a weak reference to its "owner".

Unowned references

Sometimes, though, we want a non-strong reference that is not optional.

There’s another Swift solution for retain cycles. Instead of marking a reference as weak, you can mark it as unowned. This approach is useful in special cases where one object absolutely cannot exist without a reference to another, but where this reference need not be a strong reference.

For example, let’s pretend that a Boy may or may not have a Dog, but every Dog must have a Boy — and so I’ll give Dog an init(boy:) initializer. The Dog needs a reference to its Boy, and the Boy needs a reference to his Dog if he has one; that’s potentially a retain cycle:

func testUnowned() {
    class Boy {
        var dog : Dog?

        deinit {
            print("farewell from Boy")
        } 
    }
    class Dog {
        let boy : Boy
        init(boy:Boy) { 
            self.boy = boy 
        } 
        deinit {
            print("farewell from Dog")
        }
    }
    let b = Boy()
    let d = Dog(boy: b) 
    b.dog = d
}
testUnowned() 
// nothing in console

We can solve this by declaring Dog’s boy property unowned:

func testUnowned() {
    class Boy {
        var dog : Dog?
        deinit {
            print("farewell from Boy")
        }
    }
    class Dog {
        unowned let boy : Boy 
        init(boy:Boy) { 
            self.boy = boy 
        } 
        deinit {
            print("farewell from Dog")
        }
    }
    let b = Boy()
    let d = Dog(boy: b)
    b.dog = d 
}
testUnowned() 
// farewell from Boy, farewell from Dog

An advantage of an unowned reference is that it doesn’t have to be an Optional and it can be a constant (let). But an unowned reference is also genuinely dangerous, because the object referred to can go out of existence behind the referrer’s back, and an attempt to use that reference will cause a crash, as I can demonstrate by this rather forced code:

var b = Optional(Boy())
let d = Dog(boy: b!)
b = nil // destroy the Boy behind the Dog's back 
print(d.boy) // crash

Clearly you should use unowned only if you are absolutely certain that the object referred to will outlive the referrer.

Choosing between Unowned and Weak References

Should you prefer unowned or weak references in your own APIs? Ultimately, this question boils down to the lifetimes of the objects involved. If the objects have independent lifetimes — that is, if you can’t make any assumptions about which object will outlive the other — a weak reference is the only safe choice.

On the other hand, if you can guarantee that the non-strongly referenced object has the same lifetime as its counterpart or will always outlive it, an unowned reference is often more convenient. This is because it doesn’t have to be optional and the variable can be declared with let, whereas weak references must always be optional vars. Same-lifetime situations are very common, especially when the two objects have a parent-child relationship. When the parent controls the child’s lifetime with a strong reference and you can guarantee that no other objects know about the child, the child’s back reference to its parent can always be unowned.

Unowned references also have less overhead than weak references, so accessing a property or calling a method on an unowned reference will be slightly faster. That said, this should only be a factor in very performance-critical code paths. The downside of preferring unowned references is, of course, that your program may crash if you make a mistake in your lifetime assumptions. Personally, we often find ourselves preferring weak even when unowned could be used because the former forces us to explicitly check if the reference is still valid at every point of use. Especially when refactoring code, it’s easy to break previous lifetime assumptions and introduce crashing bugs.

But there’s also an argument to be made for always using the modifier that captures the lifetime characteristics you expect your code to have in order to make them explicit. If you or someone else later changes the code in a way that invalidates those assumptions, a hard crash is arguably the sensible way to alert you to the problem — assuming you find the bug during testing.