Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to know if the closure is owned by the class?

Tags:

closures

swift

I am having hard time figuring out how to make sure when to use [weak self]/[unowned self] in the closure body. In the two scenarios shown below, according to me, it depends upon if the class B owns the passed closure or not.

Now If the implementation of class B is hidden I am not really sure how to decide on using [weak self]/[unowned self].

Can someone please help me understand how you will decide ?

/******** Scenario 1 **********/

class A {
    var b:B?
    let p = "Some Property of A"

    init() {
        print("Init of A")

        self.b = B(closure: { (number) -> Void in
            print(self.p)       // capturing self but still no need to write [weak/unowned self]
            print(number)
        })
    }

    deinit {
        print("Deinit of A")
    }
}

// Suppose this is a library class whose implementation is hidden
class B {
    init(closure:(Int->Void)) {
        print("Init of B")
        // ... do some work here
        closure(20)
    }
    deinit {
        print("Deinit of B")
    }
}

var a:A? = A()
a = nil

Output:

//    Init of A
//    Init of B
//    Some Property of A
//    20
//    Deinit of A
//    Deinit of B

Now the second scenario which will cause the reference cycle.

/******** Scenario 2 **********/

class A {
    var b:B?
    let p = "Some Property of A"
    init() {
        print("Init of A")

        self.b = B(closure: { (number) -> Void in
            print(self.p)       // capturing self but NEED to write [weak/unowned self]
            print(number)
        })
    }

    deinit {
        print("Deinit of A")
    }
}

// Suppose this is a library class whose implementation is hidden
class B {
    let closure:(Int->Void)

    init(closure:(Int->Void)) {
        print("Init of B")
        self.closure = closure     //class B owns the closure here
        f()
    }

    func f() {
        self.closure(20)
    }
    deinit {
        print("Deinit of B")
    }
}

var a:A? = A()
a = nil 
like image 605
Robin Pahwa Avatar asked Sep 20 '15 12:09

Robin Pahwa


1 Answers

The idea of "owning" is probably the wrong terminology here. Objective-C & Swift use ARC to manage memory. It's a system of references of varying types (strong, weak, and unowned). And it's important to note that unless a reference is marked as being weak or unowned, it's strong.


So, let's start by taking a look at your first example and pay attention to your references.

Below your class declarations, we have this code:

var a:A? = A()
a = nil

Because a isn't marked as weak or unowned, it's a strong reference to the object that we also create in the same line that we declare a. So, a is a strong reference to this object until a no longer points to that object (which in this case happens in the second line). And as we know, strong references prevent deallocation from happening.

Now let's dive into A's init method, which we're actually calling in this line.

init() {
    print("Init of A")

    self.b = B(closure: { (number) -> Void in
        print(self.p)
        print(number)
    })
}

The first thing A's init does is print "Init of A", and that's the first thing we see looking at what is printed.

The next thing it does is assign a value to it's b property. It's b property also isn't marked as weak or unowned, so this is a strong reference.

And the value it is assigning to b is a newly constructed instance of the B class, which explains the second line we see printed: "Init of B", as that's the first line of B's initializer.

But B's initializer takes a closure. Here's the closure we've passed it:

{ (number) -> Void in
    print(self.p)       // capturing self but still no need to write [weak/unowned self]
    print(number)
}

This block absolutely does hold a strong reference to self (in this case, the instance of a which printed "Init of A" earlier.

So, why isn't there a retain cycle? Well, let's look at B's initializer. What does it do with the closure?

class B {
    init(closure:(Int->Void)) {
        print("Init of B")
        // ... do some work here
        closure(20)
    }
    deinit {
        print("Deinit of B")
    }
}

So, when we instantiate an instance of B, it fires the closure, then forgets about it. No strong reference is ever made to the closure which we passed in.

So, let's take a look at our references:

 global --strong--> a
closure --strong--> a
      a --strong--> b

So, b will continue to have a strong reference and continue to exist for as long as a exists and maintains its strong reference to it. And a will continue to exist as long as at least one thing between your global reference and the closure continue to exist and maintain their strong reference to it.

But notice, nothing is keeping a strong reference to the closure. At least, not beyond the scope of whatever method it is used in.

The B initializer maintains a strong reference to the closure passed into it only until the end of the initializer.

So, when we write this line:

var a:A? = A()

By the time A() has returned, the variable a remains the only strong reference to a, and a remains the only strong reference to b. The closure, which had the potential to create a reference cycle no longer exists. Then, when we set a to be nil, we kill our strong reference to a.

a = nil

So, a deallocates. Upon that happening, there remain no strong references to b, so it also deallocates.


Your second example is different. In the second example, the implementation of A remains the same, but the implementation of B has changed. B now has this closure property which keeps a strong reference to whatever closure is passed into B's initializer.

So now, our references look like this:

 global --strong--> a
closure --strong--> a
      a --strong--> b
      b --strong--> closure

So you can see, even if we break the global reference to a, there still exists a retain cycle:

a --> b --> closure --> a

If we do not make use of [weak self] or [unowned self], the closure absolutely has a strong reference to self. Whether or not that creates a retain cycle depends on what has strong references to the closure.

In order to determine that for 3rd party libraries, start by checking the source code or documentation. Outside of Apple, I don't currently know how to distribute a Swift library with private implementation that we can't investigate, and Apple's code is all well documented. But assuming the worst, assuming we really have no means, then treat any closure you pass to the third party library as something that the library will hold a strong reference to.

Even this doesn't necessarily mean we must always use [weak self] or [unowned self] in our closures.

If we notice in the example above, there are multiple ways to break the retain cycle. Remember what it looks like:

a -> b -> closure -> a

So, using [weak self] or [unowned self] would prevent the retain cycle as it would eliminate the closure's strong reference to a. But even if the closure maintains a strong reference to a, notice that if we break a's strong reference to b that the cycle breaks. Nothing holds a strong reference b so b will deallocate. That will leave nothing holding a strong reference to the closure, allowing the closure to deallocate, and then, nothing (at least within this cycle) is keeping a alive.

So if we pass a closure that does something like this...

{ (number) in 
    print(self.p)
    print(number)
    self.b = nil
}

That third line just so happens to break the cycle because now self no longer holds a strong reference to the b which holds the strong reference to the closure which holds the strong reference back to self.

like image 173
nhgrif Avatar answered Oct 27 '22 16:10

nhgrif