Note on Memory management in Swift. Always need [weak self]? Delayed deallocation? Get rid of confusion!
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 whileunowned
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
orUIViewPropertyAnimator.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
downloadTask
does 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 useguard let
to unwrapself
- 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!
- will create temporary strong reference to
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 captureself
➡ 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
) andself
strongly reference toFirstViewController
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 referenceDispatchQueue.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:
- https://stackoverflow.com/a/62352667
- https://stackoverflow.com/a/41992442
- https://medium.com/@almalehdev/you-dont-always-need-weak-self-a778bec505ef
- https://www.avanderlee.com/swift/weak-self/
- https://www.swiftbysundell.com/articles/capturing-objects-in-swift-closures/
- https://www.swiftbysundell.com/questions/is-weak-self-always-required/
- https://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/