Monday, 3 April 2017

Memory leak in Swift: Assigning a function to a closure

TL;DR - Assigning a function to closure property creates a strong reference to the owner of the function potentially creating a retain cycle
Swift has first class functions, meaning that a function is treated like any other object, i.e. it's possible to:
  • - Pass a function as an argument into another function 
  • - Return a function as a value from a another function
  • - Assign a function to a variable 
  • - Store functions in data structures
I have found one caveat related to this. Let's say you have a view that has a closure property that gets called when a button is pressed.
class View {
    var onButtonPressed: (()-> Void)?
}
This View is owned by a ViewController and it handles the button being pressed by assigning it's handleButtonAction function to the onButtonPressed closure property of the View.
class ViewController: UIViewController {
    let customView = View()
    
    init() {
        super.init(nibName: nil, bundle: nil)
        customView.onButtonPressed = handleButtonAction
    }
    
    required init?(coder aDecoder: NSCoder) { fatalError() }
    
    func handleButtonAction() { /* some implementation */ }
}
Since Swift allows for functions to be assigned to closures I thought that customView.onButtonPressed = handleButtonActionwas an elegant way of linking the implementation of the handler, defined in it's own function, to the button action. However, this seemingly harmless code will create a retain cycle. Thanks to Xcode 8 memory debugger it's easy to visualise this:
This happens because when the handleButtonAction function is assigned to the onButtonPressed closure self is implicitly captured since self is the owner of handleButtonAction. The strong reference to self from the handleButtonAction function is indicated by the blue arrow.

Unit testing the retain cycle

While finding a solution to break this cycle it's good to have a unit test that proves this. The following test will fail if the ViewController has a retain cycle:
func testViewControllerNotRetained() {
    // Create two variables for the view controller,
    // one strong and one weak
    var sut: ViewController? = ViewController()
    weak var weakSut = sut
    
    // Nilling out the strong reference should release the object,
    // making the weak reference also nil
    sut = nil
    XCTAssertNil(weakSut)
}

Breaking the cycle

A solution to this problem can be to assign a new block that captures self weakly and call the function inside it using the weak self:
view.onButtonPressed = { [weak self] in
    self?.handleButtonAction()
}
In this case we can go for unowned since the View will not exist beyond the lifecycle of the ViewController:
view.onButtonPressed = { [unowned self] in
    self.handleButtonAction()
}
This solution is certainly less elegant than the original direct assignment. This is why it's so easy to forget that assigning a function retains the owner of that function potentially creating a retain cycle.
I’d like to thank Nahuel Marisi and Neil Horton for reviewing this article.

1 comment: