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!!!")
}
}
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.
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.