Do protocols have an effect on the retain count?

2019-07-14 13:25发布

问题:

I have a very simple code. I’m PURPOSELY creating a memory cycle with a delegate. Trying to observe and learn how to use Xcode's Memory Graph.

What I don’t get is why in the connections sections, Xcode says there are 3 connections. There should only be 2.

If I create a memory cycle with with closures, then it will show 2 connections.

My code for leaking with delegate:

protocol SomeDelegate {
    func didFinishSomething()
}

class Something {
    var delegate: SomeDelegate?
}

class ViewController: UIViewController, SomeDelegate {

    var x = Something()

    override func viewDidLoad() {
        super.viewDidLoad()
        print("view did load")
        x.delegate = self
    }

    func didFinishSomething() {
        print("something was finished")
    }

    deinit {
        print("dellocated!!!")
    }
}

I push that view into a navigationController then go back.

The 2 delegate objects have a slightly different memory addresses, it differs just by a +16


It seems that it has something to do with delegate object being a protocol. Because when I removed the protocol, then it reduced down to 2:

class Something2 {
    var delegate: ViewController?
}

class ViewController: UIViewController {

    var x = Something2()

    override func viewDidLoad() {
        super.viewDidLoad()
        print("view did load")
        x.delegate = self
    }

    deinit {
        print("dellocated!!!")
    }
}

回答1:

I'm pretty sure that this is just Xcode getting confused. A protocol value shouldn't cause any extra retains over a normal strong reference, as memory management operations are forwarded to the underlying value through the type metadata stored within the existential container.

Here's a minimal example that reproduces the same result in Xcode's memory graph debugger:

protocol P {}

class C : P {
  var d: D?
}

class D {
  var p: P?
}

func foo() {
  let c = C()
  let d = D()
  c.d = d
  d.p = c
}

foo()

print("insert breakpoint V")

print("insert breakpoint ^")

If you insert a breakpoint between the print statements and look at the memory graph, you'll see 3 connections. Interestingly enough, if you assign c.d after you assign d.p, you'll see the correct result of 2 connections instead.

However if we set symbolic breakpoints on swift_retain and swift_release in order to see the strong retain/release Swift ARC traffic (while printing out the value stored in the %rdi register, which appears to be the register used to pass the argument on x86):

and then insert a breakpoint immediately after the call to foo(), we can see that in both cases the instances each get +2 retained and -2 released (bearing in mind they enter the world as +1 retained, thus keeping them allocated):

swift_retain 1
     rdi = 0x000000010070fcd0
swift_retain 2
     rdi = 0x000000010070fcd0
swift_release 1
     rdi = 0x0000000000000000
swift_release 2
     rdi = 0x000000010070fcd0
swift_retain 3
     rdi = 0x00000001007084e0
swift_retain 4
     rdi = 0x00000001007084e0
swift_release 3
     rdi = 0x00000001007084e0
swift_release 4
     rdi = 0x000000010070fcd0
swift_release 5
     rdi = 0x00000001007084e0

So it looks like Xcode is at fault here, not Swift.



回答2:

Yes, the existential container that is used to implement protocol-typed variables can generate an extra retain. See Understanding Swift Performance for various implementation details. 16 bytes (2 machine words) is the header of the existential container.