Memory Management in Swift Closures
Ubuy India
We are an online e-commerce portal which is delivering products to India from around the world.
Memory management is a big topic in Swift and iOS development. If there are plenty of tutorials explaining when to use weak self with closure, here is a short story when memory leaks can still happen with it.
For the purpose of this blog post, let’s assume we have the following class with two functions. Each function execute something and finalize the execution with a closure executed.
?I have revisited the examples to highlight when the reference counter is increased and when it can cause memory leaks.
class MyClass {
???func doSomething(_ completion: (() -> Void)?) {
???????// do something
???????completion?()
???}
???func doSomethingElse(_ completion: (() -> Void)?) {
???????// do something else
???????completion?()
???}
}
Now, here comes a new requirement, we want a new function doEverything that will execute both doSomething and doSomethingElse in this order. Along the way, we are changing the state of the class to follow the progression. var didSomething: Bool = false
var didSomethingElse: Bool = false func doEverything() {
???self.doSomething {?
???????self.didSomething = true // <- strong reference to self
???????print("did something")
???????self.doSomethingElse {
???????????self.didSomethingElse = true // <- strong reference to self
???????????print("did something else")
???????}
???}
} Right off the bat, we can see self is strongly captured in the first and second closures: the closures keep a strong reference to self which internally increment the reference counter and can prevent to de-allocate the instance during the execution of doSomething.It means if those functions were asynchronous and we want to release the instance before it completes the execution, the system would still have to wait to complete it before releasing the memory. Of course, we know better and setup weak self for the closures:
func doEverything() {
???self.doSomething { [weak self] in?
???????self?.didSomething = true
???????print("did something")
???????self?.doSomethingElse { [weak self] in?
???????????self?.didSomethingElse = true
???????????print("did something else")
???????}
???}
}
Wait, do we actually need both [weak self] for each closure?
Actually, we don’t.When we have nested closures like here, we should always set weak self to the first one, the outer closure. The inner closure, the one nested in the outer can reuse the same weak self.
func doEverything() {
???self.doSomething { [weak self] in?
???????self?.didSomething = true
???????print("did something")
???????self?.doSomethingElse { in
???????????self?.didSomethingElse = true
???????????print("did something else")
???????}
???}
}
However, if we did the other way around, having weak self only in the nested closure, the outer closure would still capture self strongly and increment the reference counter. So be careful where you set this one up.
func doEverything() {
???self.doSomething { in?
???????self.didSomething = true // <- strong reference to self
???????print("did something")
???????self.doSomethingElse { [weak self] in?
???????????self?.didSomethingElse = true
???????????print("did something else")
???????}
???}
}
So far so good.Since we want to change other variables along the way, let’s clean the code with a guard let to make sure the instance is still available.
func doEverything() {
???self.doSomething { [weak self] in?
???????guard let self = self else {?
???????????return?
???????}
???????self.didSomething = true
???????print("did something")
???????self.doSomethingElse { in
???????????self.didSomethingElse = true // <-- strong reference?
???????????print("did something else")
???????}
???}
}
But now, here comes the question: since we have a strong reference called self in the outer closure, does the inner closure strongly capture it? How can we verify this?
This is the kind of questions that is worth diving into and Xcode Playground is perfect for this. I’ll include few logs to keep track of the steps as well as logging the reference counter.
For the first example, let’s keep it simple, so we can see how the reference counter is incremented along the way.
class MyClass {
???func doSomething(_ completion: (() -> Void)?) {
???????// do something
???????completion?()
???}
???func doSomethingElse(_ completion: (() -> Void)?) {
???????// do something else
???????completion?()
???}
???var didSomething: Bool = false
???var didSomethingElse: Bool = false
???deinit {
???????print("Deinit")
???}
???func printCounter() {
???????print(CFGetRetainCount(self))
???}
???func doEverything() {
???????print("start")
???????printCounter()
???????self.doSomething {
领英推荐
???????????self.didSomething = true
???????????print("did something")
???????????self.printCounter()
???????????self.doSomethingElse {
???????????????self.didSomethingElse = true
???????????????print("did something else")
???????????????self.printCounter()
???????????}
???????}
???????printCounter()
???}
}
do {
???let model = MyClass()
???model.doEverything()
}
Here is the output
# output
start
2
did something
4
did something else
6
2
Deinit
With only strong references to self, we can see the counter going up to 6. However, as expected, once both functions are executed, the instance is de-allocated.
Now let’s introduce weak self in the outer closure.
func doEverything() {
???print("start")
???printCounter()
???self.doSomething { [weak self] in
???????self?.didSomething = true
???????print("did something")
???????self?.printCounter()
???????self?.doSomethingElse {
???????????self?.didSomethingElse = true
???????????print("did something else")
???????????self?.printCounter()
???????}
???}
???printCounter()
}
With the first weak self, the instance is still de-allocated, and the counter goes only up to 4.
# output
start
2
did something
3
did something else
4
2
Deinit
So what happens with guard let self?
func doEverything() {
???print("start")
???printCounter()
???self.doSomething { [weak self] in
???????guard let self = self else { return }
???????self.didSomething = true
???????print("did something")
???????self.printCounter()
???????self.doSomethingElse {
???????????self.didSomethingElse = true
???????????print("did something else")
???????????self.printCounter()
???????}
???}
???printCounter()
}
Here is the output
# output
start
2
did something
3
did something else
5
2
Deinit
If the instance is successfully de-initialized, we can see that the counter is actually increased from 4 to 5 when we execute doSomethingElse, which means the nester closure capture strongly our temporary self.
Last but not least, if we have too many closures to handles, the best is still to refactor it as a separate function, or use newer API to avoid those mistakes. I’m thinking about functional reactive programming like RxSwift or Combine, but also swift Async to get by.
Of course, the code shared today might be a bit farfetched and might not reflect your daily usage of closures, but in my opinion, it’s still important to keep in mind the memory management and reference we make of our instances. Also, this question came right into the middle of a peer review, so we never too careful ;)