iOS development Weak and Strong Dance: How it breaks cycle reference?

blitzdex27

Dekstur

Posted on August 13, 2023

iOS development Weak and Strong Dance: How it breaks cycle reference?

iOS development weak and strong dance cover

This article will try to describe as simple as possible the cycle reference, and the behavior of the closure when capturing. Once we have understand these things, we will proceed on how we can break the cycle reference that causes memory leaks.

This article will use simple language to describe things, and will not introduce complex words. The objective is to get an insight on what is happening, rather than being drifted on deep technical complexities and terms.

We will not use escaping closures here. You can infer how it will work in escaping closures. Determining the need to use weak and strong dance depends on your context, and will not be covered in this article. You can read You don’t (always) need [weak self] | by Besher Al Maleh | Medium if you are interested.

What is a cycle reference?

It is when an instance A owns instance B, and instance B owns instance A. Thereby, creating cycle reference.

Why is cycle reference bad?

Because when a variable holding the instance A strongly becomes nil, instance A will still be alive. While it is still alive in the memory, we have no way to access nor destroy it. If we continually create instances of it, they will eventually stack up in the memory, causing memory leak.

Here is an example:

class Person {
  var dog: Dog?
  func adoptAdog(_ dog: Dog) {
    self.dog = dog
    dog.owner = self // cycle reference!
  }
}
class Dog {
  var owner: Person?
}

func simulateAdoptingAdog() {
  let person = Person()
  let dog = Dog()
  person.adoptAdog(dog)
}

simulateAdoptingAdog() // Instances in memory: Person=1, Dog=1
simulateAdoptingAdog() // Instances in memory: Person=2, Dog=2
simulateAdoptingAdog() // Instances in memory: Person=3, Dog=3
Enter fullscreen mode Exit fullscreen mode

You noticed that inside the simulateAdoptingAdog method we have created a Person and Dog instances, and used adoptAdog method that causes cycle reference. The two instances held strong references to each other.

We can avoid this using weak attribute:

class Dog {
  weak var owner: Person?
}
Enter fullscreen mode Exit fullscreen mode

Now, the Dog instance will not hold the Person instance strongly once it is assigned.

In this article, we will talk about the cycle reference introduced by closures. On the Person and Dog classes, we can see clearly the flow on how a cycle reference is introduced. However, in closure it is implicit. By knowing the behavior of a closure, we will know how we can avoid cycle reference.

What is a weak and strong dance?

The weak and strong dance received a name because developers often use it. If you do not want to think of complexities of cycle reference in a closure, developers just use it. This is when an instance is captured by the closure weakly, and will be held strongly be the local variable of the closure’s implementation. Sometimes you can see them on Objective-C code as weakify and strongify macros. In Swift, we do it like this:

let someClosure = { [weak self] in
  guard let self = self else { return }
  // code here
}
Enter fullscreen mode Exit fullscreen mode

What is the specific reason of using weak and strong dance?

There are at least two reasons:

  • Break the cycle reference.

  • Keep the instance alive until the implementation of the closure is finished.

How can we tell if the closure’s implementation has finished?

There are a lot of reasons an implementation may come to an end. I will only mention three basic reasons here:

  • Last line of code has finished executing

  • A return keyword is called

  • A throw keyword is called

let someClosure = { [weak self] in
  guard let self = self else { 
    return // will end the implmentation
  }

  guard !self.name.isEmpty else {
    throw SomeError.nameEmpty // will end the implementation
  }

  print(self.name)
  ... // some lines of code here
  print(self.name) // after this line is executed, the implementation is considered finished
}
Enter fullscreen mode Exit fullscreen mode

How can we tell if a closure introduced a cycle reference?

It is when the owner of the closure is used inside the closure, thereby capturing the owner’s instance and hold it strongly.

class Person {
  lazy var goToWorkClosure = {
    print("pay the bus")
    self.rideTheBus() // cycle reference!
  }
  func rideTheBus() {
    print("ride the bus")
  }
}

var person: Person? = Person()
person?.goToWorkClosure()  // [1]
person = nil // [2]
Enter fullscreen mode Exit fullscreen mode
  • [1] — once this line of code is executed, an instance of a closure will be created. The closure is strongly referenced by the Person’s instance property of goToWorkClosure. Therefore, the closure instance is owned by the Person instance. While the Person instance is also owned by the closure. Thereby, creating a reference cycle.

  • [2] — Person instance will not be destroyed. If that is not destroyed, the closure will also not be destroyed.

Why closure captures the instance strongly?

Because we used self inside the closure by calling self.rideABus.

A simple explanation is because closures, by default, make sure the outside variable’s instances used inside its implementation is alive. So once it was executed, it can still use those instances. Therefore, it captures those instances strongly.

How to modify this default behavior?

We can modify this behavior by explicitly defining the capture list. Normally, closure implicitly make a capture list with strong references.

With capture list, we can tell it to capture a certain variable weakly:

class Person {
  lazy var goToWorkClosure = { [weak self]
    print("pay the bus")
    self?.rideTheBus()
  }
  func rideTheBus() {
    print("ride the bus")
  }
}

var person: Person? = Person()
person?.goToWorkClosure()  // [1]
person = nil // [2]
Enter fullscreen mode Exit fullscreen mode
  • [1] — no reference cycle will be introduced upon calling this method

  • [2] — Person instance will be destroyed together with the instances it holds (in this case it only holds the closure instance)

This will break the cycle reference, similar to the first example I have introduced with Person and Dog classes.

You will notice that we have used self? when calling the rideTheBuss method. This is because self can be nil at any time. If self is nil at the time it is called, the rideTheBus will not execute.

Though the point where the self?.rideTheBus() line of code will try to execute will never occur, because this closure will be destroyed as soon as the self is deallocated or destroyed.

But if you have a closure dispatched on another thread, making it run asynchronously, the closure will be owned by that thread until the implementation is finished. And if that closure contains [weak self], it will execute the self?.rideTheBus(). I mean with executing this line of code is that it will check first if self is existing before calling the rideTheBus method. And if not, the method will not execute.

Using the [weak self], the closure will capture self weakly. This is safe, however, other codes in the implementation that uses self have no guarantee that they will all run. There will be a time when first half of your code run perfectly using self, but the second half will not. Why? Because at that time the self is destroyed.

How do we make sure self is alive while the implementation is being executed?

We use the weak and strong dance! The closure captures self weakly, while the local variable on the implementation holds the self strongly.

class Person {
  lazy var goToWorkClosure = { [weak self]
    guard let self = self else { return }
    print("pay the bus")
    self?.rideTheBus()
  }
  func rideTheBus() {
    print("ride the bus")
  }
}

var person: Person? = Person()
person?.goToWorkClosure()
person = nil
Enter fullscreen mode Exit fullscreen mode

Take note that the self refer to the instance. And the implementation refers to the code inside the { } of the closure. Once the implementation is finished, all local variables will be destroyed. Thereby, releasing the hold on the instance.

Conclusion

Understanding the behavior of weak and self dance might be a challenge. You must understand Automatic Reference Counting (ARC) as a pre-requisite, and the behavior of closure. Since ARC is not mentioned here, I tried to explain the weak and strong dance that a beginner will understand.

If you are not a beginner and already understand ARC, you might also learn something here. There might be questions on your mind that you ignore for some quite time now. Reading this might help, or may lead you to more questions.

As follow up, I might create another one on the following weeks with more detailed example and explanations.

💖 💪 🙅 🚩
blitzdex27
Dekstur

Posted on August 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related