When to weakify

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,

  1. Our class Sample owns the closure
  2. closure, when defined in the next block captures self and creates the ownership status inside it since self essentially refers to Sample in this case
  3. This has created a retain cycle and a memory leak
  4. 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

  1. A ViewController will contain a UITableView instance
  2. A UITableView will contain a custom UITableViewCell
  3. UITableViewCell instance will contain the closure 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

  1. ViewController Sample owns the tableView (Being its subclass)
  2. tableView has a cell as its subview. So it owns the cell
  3. The cell has the closure as its property so it owns the closure
  4. If you look into closure definition in the second example, it captures (i.e. owns) self instance. (In this case Sample)
  5. 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,

  1. When NotificationCenter adds the observer on the given listener, we create an ownership constraint between NotificationCenter and the observer. (Because of observer being owned by the observer, in this case - self)

  2. 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
  1. That is another indication that another strong reference to added to self when it's referenced [inside of closure][1]

  2. Failure to detect such condition will result in self being over retained and memory leak

  3. The previous step will prevent deinit from being called even when it's time to deallocate it. Which means if someone sends the notification named Notification.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)
        }
    }
}
  1. Above example has a view controller named Sample
  2. It has a property with the name customManager which acts as a singleton for the sake of this example
  3. There is a method in CustomManager class which accepts the escaping closure as an argument

Now let's see how this causes the retain cycle,

  1. Class Sample has a property named customManager which means it owns that singleton instance

  2. When customManager instance calls doSomething method, is taken in an escaping closure as its parameters

  3. As this parameter is consumed by this method, it is retained or owned by the customManager as one of its properties

  4. Now customManager owns this closure

  5. As seen in the second example, closure captures the self inside of it

  6. 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 that customManager wouldn't be retaining it and it would be deallocated as soon as our ViewController went out of scope. But having it declared as escaping 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


  1. krakendev.io ↩︎ ↩︎ ↩︎

  2. https://stackoverflow.com/a/27123176 ↩︎