Swift Closures Memory Hell
Hi everyone, let talk about one of the simplest but trickiest things in Swift: Closures and ARC. Automatic Reference Counter is a simple topic, as well as closures. And it is easy to manage memory problems in swift. For most of the time, you just need to know what strong reference cycles(also known as “retain cycle”) are and how to prevent them(Spoiler alert! Use weak and unowned keywords in appropriate places).
However, I see many iOS developers understanding how ARC works but they suddenly forget their knowledge when they work with closures and functions. They either do not use weak and unowned or put weak in every closure they encounter. Which eventually ends up with memory leaks or weird bugs due to early object release, or sometimes I see code like this 😂:
closure {[weak self] in guard let strongSelf = self else { self?.presentError() return } ... }
So let’s dive into the code to clarify the relation between closures and ARC. You can find a source code here.
We will have a simple class named Ball, which has a stored closure. And to illustrate that Ball object deinitialized, we’re going to print “Ball destroyed” message to the console.
class Ball { private var onKickCallback: (() -> Void)? func kick() { onKickCallback?() } func onKick(callback: (() -> Void)?) { onKickCallback = callback } deinit { print("Ball destroyed") } }
Next, let’s create Player class which will work with our Ball class. Player will have a stored property of type string, and deinitializer with the print method in it.
class Player { let name: String init(name: String) { self.name = name } deinit { print("Player \(name) destroyed") } }
In the first example let the closure in Ball class capture Player class, and see the order of deinits.
let ball = Ball() if true { let john = Player(name: "John") ball.onKick { print(john.name + " kicked the ball") } } ball.kick()
So, we create a Ball object and local scoped Player object. And in that scope ball captures john object. And here is the output.
John kicked the ball Ball destroyed Player John destroyed
As we can see even though john object’s scope ended first, it released after the ball object. Which is totally fine according to ARC. And you need to keep this aspect in mind, especially when working with singletons. Because if singleton captures some reference type object, that object won’t be released until closure gets rewritten, otherwise it never deinits and remains in the memory.
Now, let’s add weak or unowned keyword, to the closure’s body and changing the last block of code. And it is going to look like this:
let ball = Ball() if true { let john = Player(name: "John") ball.onKick {[weak john] in guard let john = john else { return } print(john.name + " kicked the ball") } } ball.kick()
In this case, by using weak keyword the Ball object is not even holding a Player object. We used weak reference on the object which is not holding any reference to back to its holder. Let look and compare the outputs:
Player John destroyed Ball destroyed
So, the first thing to notice by comparing with previous output is that Ball’s method kick doesn’t print anything. Second, the order of deinitializers changed. The john property releases independently to ball object. Which means ball didn’t have any reference to john object. So, if a closure captures some reference type object with the weak or unowned keyword and that object is not holding any reference back to the holder of the closure, you should know that object isn’t likely to survive if it hits its scope end. And most of the developers make this mistake, by putting weak in every closure they write. Maybe, just in case to prevent the memory leak, but it ends up with the unexpected behavior of an app.
Next, let’s make Player class hold a reference to a Ball class. With the slight changes of Player class, the code:
class Player { let name: String let ball: Ball init(name: String, ball: Ball) { self.name = name self.ball = ball } deinit { print("Player \(name) destroyed") } }
So, the only difference is that Player owns a ball property and initializes it through init constructor method. We’re going to use the code from the first example, and see how things will change.
let ball = Ball() if true { let john = Player(name: "John", ball: ball) ball.onKick { print(john.name + " kicked the ball") } } ball.kick()
It’s almost the same code, with a slight difference in Player construction due to new ball parameter passing to the object. The output:
John kicked the ball
As you can see, objects don’t even get released from the memory. That happens because the player is holding the reference to the ball object, meanwhile, a ball object holds a reference to a player object in its closure. And this is how a retain cycle looks like. Since both objects holding each other, they can’t be destroyed. As ARC sees that there are references to the object, that object is never gets destroyed, unless the object holding the reference deinits.
To fix that issue, we will weakly capture john property in balls closure body. And our code is going to look like this:
let ball = Ball() if true { let john = Player(name: "John", ball: ball) // If you put unowned instead of weak it's going to crash ball.onKick { [weak john] in guard let john = john else { return } print(john.name + " kicked the ball") } } ball.kick()
As you can see we capture john object weakly. And this code will crash if try to use unowned instead of weak. Because once john hits the end of its scope it deinits. But the ball object will remain alive, and by calling onKick method it will inevitably use dead john reference. So, be careful with those things. Output:
Player John destroyed Ball destroyed
We don’t see John kicking the ball, because he didn’t survive after hitting the end of his scope.
Conclusion
Before we move to the key points, I have a good association for how ARC operates for you: It’s like I’m holding you and you’re holding me, so we both can’t fall. And if you don’t hold me, but I’m still holding you, you won’t fall. But in case, if I fall, we both fall. Maybe that will help you to understand =)
And here is the source code if you need: https://github.com/utemissov/closures
The key points are:
- Do not forget that ARC principles apply to closures, functions, and reference type objects
- If any property or method of the class is holding any reference to any other reference type object that means your class captured that reference. Functions will start holding the reference only once they get called.
- Captured reference will not deinit until the object holding it releases that object or releases itself.
- Use weak and unowned wisely. Use them only if you have two objects holding references to each other and use it by looking at the context, by understanding which one should look at another weakly.
- If you’re using a singleton and that singleton captures some other object, you need to think about the time when to release that object from the memory
Thank you!
Subscribe to be notified of new posts!