Note on Memory management in Swift. Always need [weak self]? Delayed deallocation? Get rid of confusion!

6 minute read

PROBLEM:

 1class ProductViewController: UIViewController {
 2    private lazy var buyButton = Button()
 3    private let purchaseController: PurchaseController
 4    
 5    ...
 6
 7    override func viewDidLoad() {
 8        super.viewDidLoad()
 9
10        // Since our buyButton retains its closure, and our
11        // view controller in turn retains that button, we'll
12        // end up with a retain cycle by capturing self here:
13        buyButton.handler = {
14            self.showShoppingCart()
15            self.purchaseController.startPurchasingProcess()
16        }
17    }
18
19    private func showShoppingCart() {
20        ...
21    }
22}

There are two main reasons weak is useful:

  • To prevent retain cycles.
  • To prevent objects living longer than they should be.

Weak vs Unowned

  • weak is declared as an Optional while unowned is not.
  • By declaring it weak you get to handle the case that it might be nil inside the closure at some point.
  • If you try to access an unowned variable that happens to be nil, it will crash the whole program.
  • Only use unowned when you are positive that variable will always be around while the closure is around.

Escaping closure - Non-escaping closure

  • @escaping closure, can be stored, passed around and executed later.
  • @nonescaping closure, executed in scope, immediately, can NOT be stored and run later

Retain cycles

Nonescaping closure > NO retain cycles! Escaping closure, potentially have retain cycles once:

  • Closure is stored in a property or passed to another closure
  • Object inside that closure maintains a strong reference to the closure (for example, self)

Delay deallocation

  • By default, closure capture strongly all objects in its body (e.g. self)
  • Memory of these objects will not be cleaned up while closure is STILL ALIVE
  • Scenarios that keep the closure scope alive:
    • A closure (escaping or non-escaping) might be doing some expensive serial operations, thereby delaying its scope from returning until all the work is completed…
    • An escaping closure waits for a callback with a long timeout.
    • A closure (escaping or non-escaping) might employ some thread blocking mechanism (such as DispatchSemaphore) that can delay or prevent its scope from returning
    • An escaping closure might be scheduled to execute after a delay (e.g. DispatchQueue.asyncAfter or UIViewPropertyAnimator.startAnimation(afterDelay:))

Example:

 1func delayedAllocAsyncCall() {
 2    let url = URL(string: "https://www.google.com:81")!
 3
 4    let sessionConfig = URLSessionConfiguration.default
 5    sessionConfig.timeoutIntervalForRequest = 999.0
 6    sessionConfig.timeoutIntervalForResource = 999.0
 7    let session = URLSession(configuration: sessionConfig)
 8
 9    let demoTask = session.downloadTask(with: url) { localURL, _, error in
10        guard let localURL = localURL else { return }
11        let contents = (try? String(contentsOf: localURL)) ?? "No contents"
12        print(contents)
13        print(self.view.description)
14    }
15    demoTask.resume()
16}

What’s going on?

  • demoTask run immediately, not be stored anywhere else in the class.
  • the request got the time out of 999s
  • completion handler of downloadTaskdoes NOT cause retain cycle.
  • BUT…
    • while running the app, downloadTask is running but is not completed… but we dismiss the view controller
    • the closure strongly capture self (line 13) ⇢ its memory will not be cleaned.
    • SOLUTION: use [weak self] here… DO NOT use [unowned self] as the app get crashed.

Another example:

 1func process(image: UIImage, completion: @escaping (UIImage?) -> Void) {
 2    DispatchQueue.global(qos: .userInteractive).async { [weak self] in
 3        guard let self = self else { return }
 4        // perform expensive sequential work on the image
 5        let rotated = self.rotate(image: image)
 6        let cropped = self.crop(image: rotated)
 7        let scaled = self.scale(image: cropped)
 8        let processedImage = self.filter(image: scaled)
 9        completion(processedImage)
10    }
11}
  • line 5, 6, 7 do serial expensive operations.
  • use [weak self] but use guard let to unwrap self
    • will create temporary strong reference to self within the closure lifecycle.
    • will prevent self from deallocating until the closure is completedly run
    • SOLUTION: use optional chaining instead. it will check nil for self in every method call at line 5, 6, 7, if self == nil, skip!

Timers

1func leakyTimer() {
2    let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
3        let currentColor = self.view.backgroundColor
4        self.view.backgroundColor = currentColor == .red ? .blue : .red
5    }
6    timer.tolerance = 0.1
7    RunLoop.current.add(timer, forMode: RunLoop.Mode.common)
8}
  • it REPEATS
  • closure does NOT use capture list [weak self] > strongly capture self ➡ delay deallocation
  • so, use [weak self] here and invalidate the timer when it is no longer needed.

Store a function in a variable

 1class FirstViewController: UIViewController {
 2  var closure: (() -> Void)?
 3}
 4
 5class SecondViewController: UIViewController {
 6  var firstVC = FirstViewController()
 7
 8  func setupClosure() {
 9    firstVC.closure = printer
10  }
11
12  func printer() {
13    print(self.view.description) 
14  }
15}
  • This causes retain cycle!
  • line 9, closure strongly capture self (self.printer) and self strongly reference to FirstViewController that own the closure
  • ** SOLUTION:**
1  func setupClosure() {
2    firstVC.closure = { [weak self] in
3        self?.printer()
4    }
5  }
Nested closures

capture list [weak self] once in the outer block is enough. Else, if not using capture list here, retain cycle is made.

1parentFunction { [weak self] in 
2    self?.childFunction {
3        self?.foo()
4    }
5}

If use guard let self = self inside, have to capture list [weak self] to the inner block

1parentFunction { [weak self] in 
2    guard let self = self else { return }
3    self.childFunction { [weak self] in // need another here
4        self?.foo()
5    }
6}

Full example:

 1class Experiment {
 2    
 3    var _someFunctionWithTrailingClosure: (() -> ())?
 4    var _anotherFunctionWithTrailingClosure: (() -> ())?
 5    
 6    func someFunctionWithTrailingClosure(closure: @escaping () -> Void) {
 7        print("starting", #function)
 8        _someFunctionWithTrailingClosure = closure
 9        
10        // Nothing happens as retain cycle if no `[weak self]` here
11        // But it will cause delayed allocation, self will live until the block completes.
12        DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
13            self?._someFunctionWithTrailingClosure?()
14            print("finishing", #function)
15        }
16    }
17
18    func anotherFunctionWithTrailingClosure(closure: @escaping () -> Void) {
19        print("starting", #function)
20        _anotherFunctionWithTrailingClosure = closure
21        
22        DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
23            self?._anotherFunctionWithTrailingClosure?()
24            print("finishing", #function)
25        }
26    }
27
28    func doSomething() {
29        print(#function)
30    }
31
32    func testCompletionHandlers() {
33        someFunctionWithTrailingClosure { [weak self] in
34            guard let self = self else { return }
35            self.anotherFunctionWithTrailingClosure { [weak self] in
36                self?.doSomething()
37            }
38        }
39    }
40
41    // to track when the object is deallocated
42    // if `deinit` does not get called, retain cycle is happened.
43    deinit {
44        print("deinit")
45    }
46}
47
48func performExperiment() {
49    DispatchQueue.global().async {
50        let obj = Experiment()
51        obj.testCompletionHandlers()
52
53        // sleep long enough for `anotherFunctionWithTrailingClosure` to start, but not yet call its completion handler
54        Thread.sleep(forTimeInterval: 7)
55    }
56}
57
58performExperiment()

Grand Dispatch Queue

 1func nonLeakyDispatchQueue() {
 2    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
 3        self.view.backgroundColor = .red
 4    }
 5
 6    DispatchQueue.main.async {
 7        self.view.backgroundColor = .red
 8    }
 9
10    DispatchQueue.global(qos: .background).async {
11        print(self.navigationItem.description)
12    }
13}
  • Do NOT cause retain cycle, even when the closure strongly capture self
  • Because self does not reference DispatchQueue.main
  • Only when store them to variables that belong to self
1func leakyDispatchQueue() {
2    let workItem = DispatchWorkItem { self.view.backgroundColor = .red }
3    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: workItem)
4    self.workItem = workItem // stored in a property
5}

Alternatives to weak

  • Define a tuple which contains only objects needed for the closure
 1let context = (
 2    parser: parser,
 3    schema: schema,
 4    titleLabel: titleLabel,
 5    textLabel: textLabel
 6)
 7
 8dataLoader.loadData(from: url) { data in
 9    // We can now use the context instead of having to capture 'self'
10    let model = try context.parser.parse(data, using: context.schema)
11    context.titleLabel.text = model.title
12    context.textLabel.text = model.text
13}

Learned from: