swiftretain-cyclestrong-reference-cycle

Weird weak self and retain cycle behaviour


Let's consider following code:

// Just for easier testing
protocol Printer {
    var delayer: Delayer { get }
}

// Retain cycle
class Printer1: Printer {
    private func action() {
        print("action")
    }
    
    private(set) lazy var delayer: Delayer = {
        return Delayer(action)
    }()
    
    deinit {
        print("deinit")
    }
}

// Works fine, but weak mess
class Printer2: Printer {
    private func action() {
        print("action")
    }
    
    private(set) lazy var delayer: Delayer = {
        return Delayer { [weak self] in self?.action() }
    }()
    
    deinit {
        print("deinit")
    }
}

// Questionable hack, but works fine
class Printer3: Printer {
    private func action() {
        print("action")
    }
    
    private(set) lazy var delayer: Delayer = {
        return Delayer(weakAction)
    }()

    // computed property or function is also fine here
    private lazy var weakAction: () -> Void = {
        return { [weak self] in
            self?.action()
        }
    }()
    
    deinit {
        print("deinit")
    }
}

// Retain cycle
class Printer4: Printer {
    private func action() {
        print("action")
    }
    
    private(set) lazy var delayer: Delayer = {
        weak var welf: Printer4? = self
        return Delayer(welf?.action ?? {})
    }()
    
    deinit {
        print("deinit")
    }
}

// Works fine
class Printer5: Printer {
    private func action() {
        print("action")
    }
    
    private(set) lazy var delayer: Delayer = {
        weak var welf: Printer5? = self
        return Delayer { welf?.action() }
    }()
    
    deinit {
        print("deinit")
    }
}

class Delayer {
    private var action: () -> Void
    
    init(_ action: @escaping () -> Void) {
        self.action = action
    }
    
    func run() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
            self?.action()
        }
    }
}

So we have a Printer class which contains a Delayer class that takes the action on Printer and performs it delayed.

We call it something like this:

var printer: Printer? = PrinterX()

printer?.delayer.run()

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
     printer = nil
}

It is clear why Printer1 creates retain cycle. Action is passed into delayer with implicit strong self which cannot be released because Delayer is owned by Printer.

Printer2 is the intended way in my opinion. Obviously doesn't create retain cycle, but it is kind of mess to write all the time. Thats why I started experimenting with other solution.

I don't understand why Printer3 doesn't create retain cycle. Because weakAction is property on self, so passing it like that into Delayer should create strong reference like in Printer1.

I also don't understand why Priner4 does create retain cycle. welf is local weak reference to self, so it should not increase the reference count when passing it into the Delayer.

Strangely enough using the welf inside closure in Printer5 doesn't create retain cycle.

Questions

  1. Can anyone please explain to me this weird behavior on Printer3, Printer4 and Printer5
  2. I am tempted to use the Printer3 solution. Is it safe to use? As it seems almost like a bug, can I use it without worrying about it being fixed in future versions and therefore creating retain cycle in my app?

Solution

  • First of all, all printers are creating and retaining their own Delayer. The delayer takes a closure and, in turn, retains that closure.

    Let's try to walk through them one by one.

    Printer1

    As you stated yourself, it's pretty clear why it's creating a retain cycle. You are passing the self.action instance method as the closure to the Delayer, and since all closures are reference types, passing self.action will retain its surrounding scope (which is Printer1).

    Printer2

    Again, pretty obvious here. You're explicitly capturing a weak reference to self inside the closure you're passing to Delayer, hence not creating a retain cycle.

    Printer3

    Here, a retain cycle is not created, because the self.weakAction property is called immediately, and its result (a closure which holds a weak reference to self) is passed on to Delayer. This, in effect, is the exact same thing as what's happening in Printer2.

    Printer4

    First, you're capturing a weak reference to self, and then fetching welf?.action and passing the result into Delayer. Again, welf?.action is called immediately, and the result (a pointer to an instance method) is passed on to Delayer. The weak reference to self is only kept for the duration of the surrounding scope (the lazy var creation scope), and passing the action instance method will retain self. This is identical to Printer1.

    Printer5

    Here, you're first creating a weak reference to self, and then you're capturing that weak reference inside a new closure that is passed to Delayer. Since self is never directly referenced in the passed closure, it will not capture self in that scope, only the welf weak reference. This is pretty much identical to Printer2, but with a slightly different syntax.

    Personally, I would opt for the Printer2 way (creating a new closure, retaining a weak reference to self and using that to call self?.action). It makes for the easiest code to follow (as opposed to retaining a variable with a closure that weakly captures self). But, depending on what you're actual use case is, it might of course make sense.