Okay so we all know that in traditional concurrency in Swift, if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self]
in, like this:
func performRequest() {
apiClient.performRequest { [weak self] result in
self?.handleResult(result)
}
}
This is to stop us strongly capturing self
in the closure and causing unnecessary retention/inadvertently referencing other entities that have dropped out of memory already.
How about in async/await? I'm seeing conflicting things online so I'm just going to post two examples to the community and see what you think about both:
class AsyncClass {
func function1() async {
let result = await performNetworkRequestAsync()
self.printSomething()
}
func function2() {
Task { [weak self] in
let result = await performNetworkRequestAsync()
self?.printSomething()
}
}
func function3() {
apiClient.performRequest { [weak self] result in
self?.printSomething()
}
}
func printSomething() {
print("Something")
}
}
function3
is straightforward - old fashioned concurrency means using [weak self]
.
function2
I think is right, because we're still capturing things in a closure so we should use [weak self]
.
function1
is this just handled by Swift, or should I be doing something special here?
CodePudding user response:
if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self] in, like this
This isn't quite true. When you create a closure in Swift, the variables that the closure references, or "closes over", are retained by default, to ensure that those objects are valid to use when the closure is called. This includes self
, when self
is referenced inside of the closure.
The typical retain cycle that you want to avoid requires two things:
- The closure retains
self
, and self
retains the closure back
The retain cycle happens if self
holds on to the closure strongly, and the closure holds on to self
strongly — by default ARC rules with no further intervention, neither object can be released (because something has retained it), so the memory will never be freed.
There are two ways to break this cycle:
Explicitly break a link between the closure and
self
when you're done calling the closure, e.g. ifself.action
is a closure which referencesself
, assignnil
toself.action
once it's called, e.g.self.action = { /* Strongly retaining `self`! */ self.doSomething() // Explicitly break up the cycle. self.action = nil }
This isn't usually applicable because it makes
self.action
one-shot, and you also have a retain cycle until you callself.action()
. Alternatively,Have either object not retain the other. Typically, this is done by deciding which object is the owner of the other in a parent-child relationship, and typically,
self
ends up retaining the closure strongly, while the closure referencesself
weakly viaweak self
, to avoid retaining it
These rules are true regardless of what self
is, and what the closure does: whether network calls, animation callbacks, etc.
With your original code, you only actually have a retain cycle if apiClient
is a member of self
, and holds on to the closure for the duration of the network request:
func performRequest() {
apiClient.performRequest { [weak self] result in
self?.handleResult(result)
}
}
If the closure is actually dispatched elsewhere (e.g., apiClient
does not retain the closure directly), then you don't actually need [weak self]
, because there was never a cycle to begin with!
The rules are exactly the same with Swift concurrency and Task
:
- The closure you pass into a
Task
to initialize it with retains the objects it references by default (unless you use[weak ...]
) Task
holds on to the closure for the duration of the task (i.e., while it's executing)- You will have a retain cycle if
self
holds on to theTask
for the duration of the execution
In the case of function2()
, the Task
is spun up and dispatched asynchronously, but self
does not hold on to the resulting Task
object, which means that there's no need for [weak self]
. If instead, function2()
stored the created Task
, then you would have a potential retain cycle which you'd need to break up:
class AsyncClass {
var runningTask: Task?
func function4() {
// We retain `runningTask` by default.
runningTask = Task {
// Oops, the closure retains `self`!
self.printSomething()
}
}
}
In this case, you'd either need runningTask
to be a weak var
(likely not what you want), or you'd need Task { [weak self] ...}
to avoid the task from retaining self
.
CodePudding user response:
function1
isn't "just handled"; there is nothing to handle.
func function1() async {
let result = await performNetworkRequestAsync()
self.printSomething()
}
What function1
contains are method calls. That's all it contains. Would you be concerned about memory management here if the word async
didn't appear in the method declaration? Of course not. It's just a method that calls some other methods. Then why are you concerned about it when the word async
does appear? You shouldn't be.
There might, of course, be a threading issue, and the compiler will let you know if so. But from the point of view of memory management, nothing is happening here.