When to weakify
Today I am going to talk about one of the discussion prone topics on iOS platforms. Detecting memory leaks and when to apply weakify. The topic came up while I was recently performing a code review and there were some discussions on whether there is a memory leak or not. That being happened, I decided to write a blog summarizing my experience and encounters with retain cycles and memory leaks.
Now, through this blog post let's see how we can detect the memory leak and avoid false positives.
According to the Wikipedia article,
In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released
On iOS, when an application requests a memory resource but does not relinquish it when the task is done, a memory leak happens.
How to detect memory leaks
There are two ways to detect if there is indeed a memory leak in your code. First one is more involved and sophisticated approach. In this case, you will use the memory debugger tool installed within instruments and use it to visually observer and eliminate sources of memory leaks.
The second one is easier and does not take a lot of time to detect a leak. Although, in this case finding the source might be time-consuming.
In the latter case, you can implement a deinit
method in the concerned view controller (Or UITableViewCell
subclass for that matter) and check if that method gets called on the controller is dismissed or popped. If deinit
is not getting called after such operation, then there is a strong possibility you have the retain cycle somewhere in your app which results in a memory leak.
In this article, I will be employing the second method to detect memory leaks whenever applicable.
Scenarios involving memory leaks
Closures capturing self
instance
Let's begin with an example,
Suppose we have a class named Sample
which has few properties of its own including a closure,
Class Sample: UIViewController {
var closure: (() -> Void)?
var value: Int = 0
}
And then somewhere in your codebase, you define this closure like this,
self.closure = {
print(self.value)
}
Now, this is where you've fallen into the trap. This has caused a retain cycle and let's see how,
- Our class
Sample
owns theclosure
closure
, when defined in the next block capturesself
and creates the ownership status inside it sinceself
essentially refers toSample
in this case- This has created a retain cycle and a memory leak
- Now if you want to verify it after this
ViewController
is dismissed,deinit
method, if implemented won't be called
There is a simple solution to it. You can simply weakify
the self
instance captured inside the closure to break the retain cycle thus eliminating the leak.
self.closure = { [weak self] in
print(self?.value)
}
Capturing self
inside closure owned by cell
Let's look at another example involving memory leak. This is more involved than the previous example. It will involve the UITableViewCell
, ViewController
and associated closure.
These are our initial requirements
- A
ViewController
will contain aUITableView
instance - A
UITableView
will contain a customUITableViewCell
UITableViewCell
instance will contain theclosure
property
// Custom UITableViewCell
class SampleTableViewCell: UITableViewCell {
var myClosure: (() -> Void)?
}
class Sample: UIViewController {
var value: Int = 0
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
tableView.delegate = self
tableView.dataSource = self
tableView.register(SampleTableViewCell.self, forCellReuseIdentifier: "cell")
self.view.addSubview(tableView)
}
}
extension Sample: UITableViewDataSource {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SampleTableViewCell
cell.myClosure = {
print(self.value)
}
return cell
}
Seems like we're fallen into memory leak trap one more time. But unlike the previous case, this is not obvious. Since we have closure, viewController and cell interaction this is not easily detectable. Unless of course, you implement deinit
method inside ViewController
and manually check if it's being called or not.
Let's see how
- ViewController
Sample
owns thetableView
(Being its subclass) tableView
has a cell as its subview. So it owns the cell- The cell has the closure as its property so it owns the closure
- If you look into closure definition in the second example, it captures (i.e. owns)
self
instance. (In this caseSample
) - So the last step completed the circle and has given rise to the retain cycle leading to the memory leak
Compared to the problem, it's easier to break this cycle. All we have to do it to weakify
the self
instance in the closure above leading to cycle break and eventually avoidance of memory leak.
cell.myClosure = { [weak self] in
print(self?.value)
}
Closures associated with NotificationCenter
Dealing with memory leaks in NotificationCenter
is a little bit more complicated. As of iOS 9, when you register an object as an observer you do not need to explicitly remove it unless you're using block-based NotificationCenter
This in this section we will be dealing with memory leaks associated with block-based approach since selector based approach won't contribute to any leaks.
This is my favorite case since I ran into it multiple times early in my career. This wasn't so obvious in the beginning and was only found after using advanced memory leak detection tools
Let's look at the example,
observer = NotificationCenter.default.addObserver(forName: Notification.Name("ddd"), object: nil, queue: nil) { (not) in
print(self.value)
}
// Called when ViewController is deallocated
deinit {
NotificationCenter.default.removeObserver(observer)
}
So let's look at how this is a memory leak,
-
When
NotificationCenter
adds the observer on the given listener, we create an ownership constraint betweenNotificationCenter
and the observer. (Because ofobserver
being owned by the observer, in this case -self
) -
In addition to it, if you look into the signature of
addObserver
method, it captures the escaping closure
open func addObserver(forName name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol
-
That is another indication that another strong reference to added to
self
when it's referenced [inside of closure][1] -
Failure to detect such condition will result in
self
being over retained and memory leak -
The previous step will prevent
deinit
from being called even when it's time to deallocate it. Which means if someone sends the notification namedNotification.Name("ddd")
, it will still be received by an observer even when it's not supposed to listen onto it
Just like we can weakify
self to break this cycle and get rid of memory leak,
observer = NotificationCenter.default.addObserver(forName: Notification.Name("ddd"), object: nil, queue: nil) { [weak self] (not) in
print(self?.value)
}
Escaping closure as a method argument
This is another case similar to the previous example involving escaping closures
. When dealing with escaping closures we have to be extra careful about how to we reference things inside them.
Let's look at the example,
class CustomManager {
static let sharedInstance = CustomManager()
var closure: (() -> Void)?
func doSomething(closure: @escaping () -> Void) {
self.closure = closure
self.closure?()
}
}
class Sample: UIViewController {
let customManager = CustomManager.sharedInstance
var value: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.customManager.doSomething {
print(self.value)
}
}
}
- Above example has a view controller named
Sample
- It has a property with the name
customManager
which acts as a singleton for the sake of this example - There is a method in
CustomManager
class which accepts theescaping
closure as an argument
Now let's see how this causes the retain cycle,
-
Class
Sample
has a property namedcustomManager
which means it owns that singleton instance -
When
customManager
instance callsdoSomething
method, is taken in an escaping closure as its parameters -
As this parameter is consumed by this method, it is retained or owned by the
customManager
as one of its properties -
Now
customManager
owns this closure -
As seen in the second example, closure captures the
self
inside of it -
Thus the previous step completes the circle and thus the creation of a retain cycle
Had the passed closure in
doSomething
been a non-escaping, that would've meant thatcustomManager
wouldn't be retaining it and it would be deallocated as soon as our ViewController went out of scope. But having it declared asescaping
changes a lot of things
Let's weakify
the self
instance inside escaping closure definition to break the cycle and restore some sanity,
self.customManager.doSomething { [weak self] in
print(self?.value)
}
lazy
properties as closure
This is another interesting case of hidden memory leaks. When lazy
closures are declared as lazy properties which access the instance of their owner inside the closure leading to the memory leak.
class Sample {
var value: Int = 100
lazy var myVariable: () -> Int = {
print(self.value)
return 100
}
}
In the above example Sample
retains/owns myVariable
for its lifetime and then closure retains Sample
within itself for its lifetime leading to the retain cycle. Thus attempt to dismiss or pop this viewController won't result in the automatic call to deinit
.
Now I know you're going to say that answer is use [weak self]
inside the closure. But hold on, not yet. As krakendev explains it nicely
If you know your reference is going to be zeroed out properly and your 2 references are MUTUALLY DEPENDENT on each other (one can't live without the other), then you should prefer unowned over weak, since you aren't going to want to have to deal with the overhead of your program trying to unnecessarily zero your reference pointers [1:1]
Thus as explained before, Sample
and myVariable
are really dependent on each other. Thus using assuming self
will never be nil
when used in this context. This allows us to use [unowned self]
without any anxiety.
lazy var myVariable: () -> Int = { [unowned self] in
print(self?.value)
return 100
}
Thus not only we broke the retain cycle, but we also learned one of the few use-cases where
unowned
can be used without fear.
Things which look like a memory leak, but in fact aren't
UIView
animation blocks
This is one of the cases where the practice of using [weak self]
in any closure can be substantiated. However, understanding the creation of retain cycles and their implication on memory leak scenario is important.
There are very rare instances where using weakified instance can lead to unexpected crashes, but it also helps the team to understand the right time and place to use such behavior.
UIView.animate(withDuration: 2.0) {
print(self.value)
}
In the above case, if you look into UIView
class, the animation completion block is escaping
and it owns self
inside it. So UIView
owns completion closure, which owns self
instance. However, the other side of ownership is missing in this case.
self
in this case does not own UIView
instance or any of its class variation. This we don't have a cycle and eventually a memory leak.
Dispatch queue closures
It's a similar case as the previous one,
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print(self.value)
}
DispatchQueue.main.async {
print(self.value)
}
In above examples, even though completion block is escaping
, which is owned by DispatchQueue
and it captures self
inside, there is no ownership semantics between self
and DispatchQueue
instance thus avoiding a retain cycle in other direction.
Dismiss blocks
Dismiss block can be called while dismissing view controller. Such as,
self.dismiss(animated: true) {
print(self.value)
}
self.navigationController?.dismiss(animated: true, completion: {
print(self.value)
})
This is not a retain cycle. First off, completion closure is non-escaping. Which means it is not retained by the ViewController
superclass which is responsible for calling it after ViewController
is successfully dismissed.
Also, being a non-escaping closure you don't hold a strong reference to it. Thus, the block is deallocated right after its completion. When block deallocates, it will also deallocate the captured reference (In this case, self
).
Retain cycle would've occurred if the closure had been escaping and retained by the callee of this method as a property or a variable [2]
Computed properties
Computed properties is another area that can be confused as a false positive for existance of the memory leak. Let's look at the example,
var value: Int = 100
var myVal: Int {
return self.value
}
var myClosure: () -> Void {
return {
self.value
}
}
For the above examples, they're not memory leak either even though they use self
within their computation context.
Let's try to demonstrate them in a different way which will make it more clear for them not being the cause of the memory leak,
func myVal() -> Int {
return self.value
}
func myClosure() -> (() -> Void) {
return {
self.value
}
}
Thus computed properties act more like functions declared inside the given construct. Even though self
retains computed variable during its lifetime, the computed properties do not hold self
during their lifetime. Thus avoiding the necessary condition for forming the retain cycle.
This is even more clear when you try to [weak self]
or [unowned self]
inside computed properties closure. Swift won't allow you to add that kind of code making the point obvious
lazy
properties as non-closures
Using lazy
properties as non-closures is another area which won't lead to a retain cycle. This is demonstrated in the following example, [1:2]
lazy var myVariable: Int = {
print(self.value)
return 100
}()
Although it may seem in the above example that self
owns the myVariable
for its lifetime, myVariable
closure does not retain self
forever thus eliminating the possibility of a retain cycle.
Let's see how - When myVariable
is accessed for the first time, it executes the closure and as soon as it finishes evaluating it, it destroys it and just assigns the result to myVariable
thus effectively eliminating the possibility that myVariable
ownership might be retained indefinitely.
Being a lazy variable, when subsequent attempts are made to get myVariable
, it just returns the previously computed value without evalauting the closure for rest of its lifetime.
Thus to summarize, there is no retain cycle and hence we don't need to weakify
self
here inside the closure.
References and Further Reading
https://krakendev.io
StackOverflow