Swift Retain Cycles and Strategies to Prevent Them

There are many articles written on this subject. Many of them suggest using Xcode’s memory graph debugger. Is there an easier way? I say yes! Unit tests to the rescue!

The idea of the unit test is:

  1. Instantiate the object under test (“target”) and assign it to a nullable variable.
  2. Create a weak variable (“weakTarget”) and assign the target to it
  3. On the target, call a function to test for retain cycles
  4. Set target to nil
  5. Test weakTarget for nil, if it is not nil, there is a retain cycle

For example, let’s say you have:

import UIKit

public class RetainIt {
    private var simpleObject: SimpleObject?
    
    public func letsRetain() {
        self.simpleObject = SimpleObject(item: self)
    }
}

public class SimpleObject {
    private var item: RetainIt
    
    init(item: RetainIt) {
        self.item = item
    }
}

To unit test for retain cycles, the code might look like:

public class RetainItTests: XCTestCase {
    
    public func testLetsRetain() {
        var target: RetainIt? = RetainIt()
        weak var weakTarget = target
        
        target?.letsRetain()
        
        target = nil
        
        XCTAssertNil(weakTarget, "Target was expected to be nil, a retain cycle may exist")
    }
}

Run the test and XCTAssertNil() will fail the test.

Reviewing the source code, we might see SimpleObject is retaining the RetainIt object. Let’s make item weak:

(To break the retain cycle, the strong reference needs to be broken. In this case, setting item to weak will do.)

public class SimpleObject {
    private weak var item: RetainIt?
    
    init(item: RetainIt) {
        self.item = item
    }
}

Now rerun the unit test and it should pass!

Benefits of unit testing retain cycles

  • Not sure if a function has a retain cycle? Setup a unit test! Have peace of mind knowing the code doesn’t retain.
  • If the code is retaining, a simple unit test is quick and easy to setup and run. Tweak the source code and rerun the unit test until it’s working. If the retain cycle is particularly stubborn, start commenting out source code until the unit test works. The commented out code will be where the retain cycle is at.
  • The unit test can live on as proof the function is retain cycle free. If the function is later modified, have peace of mind knowing the unit test is there to validate.

Closures – How do you know if code might retain?

Closures can strongly capture self if you’re not careful. To break the retain cycle, declare the closure block with [weak self] and then strongly capture self to use self in an atomic manner (meaning self will hang around for the life of the closure block), or use “self?”

If you answer yes to the following, you might have a retain cycle.

  • Are you in a closure?
  • Is the closure escaping? (Right click on the closure method and if you see @escaping in the definition, you have an escaping closure) – or –
  • If the closure is not escaping, is it inside a closure that is?
  • Is self referenced in the closure?

Use a unit test to validate if there is a retain cycle. If there is one, use [weak self] and “guard let self = self else { return }” to capture a strong reference to the weak self.

Weak self vs unowned self

Using either [weak self] or [unowned self] will resolve the strong self retain cycle. I would choose not to use [unowned self] since within the closure, it is the equivalent of “self!.someMethod” and if self goes nil (which someone somewhere will find a way to do it), the app will crash. One except might be is if the object with the closure is a singleton, then [unowned self] may be a fine choice.

guard self = self else { return } vs self?

public func letsRetain() {
    callMyAPI(closure: {
        self.callAMethod()
    })
}

Let’s assume callMyAPI is an escaping closure. We can expect self to get strongly captured within the closure. To break the retain cycle, we might do

callMyAPI(closure: { [weak self] in
    guard let self = self else { return }
    self.callAMethod()
    self.callBMethod()
})

// or

callMyAPI(closure: { [weak self] in
    self?.callAMethod()
    self?.callBMethod()
})

In the first case, self is made weak, then in the closure, a strong reference captures the weak self. I use this approach to make sure everything in the closure, which relies on self, is run. I like to think of it a bit like a database atomic commit – it’s all or nothing. So if self can be strongly captured, callAMethod() and callBMethod() will both be called.

The second case uses “self?”. It is a lot less code and equivalent to the first case. If you have a closure with multiple references to “self?”, self could go to nil at any time during the closure and execution would stop. So callAMethod() gets called, then self goes nil, and callBMethod() gets skipped. If this is acceptable, use “self?”. If you need both methods to execute for sure, use the first case.

Conclusion

Use unit tests to help identify retain cycles AND prevent future ones.

It can be easy to forget escaping closures can strongly capture self and will need a [weak self] to prevent retain cycles.

Avoid [unowned self] unless you know it’s safe. Otherwise, why risk “self!” and a potential app crash?

Use “guard self = self else { return }” or “self?” along with “[weak self]” to avoid retain cycles. Just remember “self?” doesn’t guarantee self will hang around for the life of your closure.

Code posted is super simplified, if a bit silly, and for example purposes only.

Leave a Reply