I was just interested to know how ARC works when a block of code goes out of scope. I assume all the reference variables are set to nil/destroyed with properties excluded and all objects are destroyed that have a 0 reference count. This scenario:
A = nil
B = nil
Or perhaps all variables and property references are set to nil going from top to bottom which would keep any retain cycles because A and B are nil before the properties are accessed.
A = nil
B = nil
A?.macbook = nil
B?.person = nil
So I was just wondering why ARC doesn't cycle through the properties first (go backwards through the code) to remove property references first, this would break any retain cycles.
A?.macbook = nil
B?.person = nil
A = nil
B = nil
I think I understand the basics of retain cycles, I'm just curious about the detail of what happens with ARC during an automated destruction phase e.g. finishing the execution of a function. Perhaps properties references aren't accessed/destroyed because they are part of the object? maybe it is too computationally complex to check every property?
var name: String
init(name: String) {
self.name = name
print("The Person \(name) is born")
}
var macbook: Macbook?
deinit {
print("The Person \(name) has died")
}
}
class Macbook {
var model: String
init(model: String) {
self.model = model
print("The \(model) Macbook is born")
}
var person: Person?
deinit {
print("The \(model) Macbook has expired")
}
}
func runTasks() {
var A: Person? = Person(name: "Reuben")
var B: Macbook? = Macbook(model: "Pro 2020")
//Setup Retain Cycle
A?.macbook = B
B?.person = A
//A?.macbook = nil
B?.person = nil //**Why doesn't ARC make this nil first to avoid retain cycles?**
A = nil
B = nil
}
runTasks()
You ask:
I'm just curious about the detail of what happens with ARC during an automated destruction phase e.g. finishing the execution of a function.
When you have a local variable that is referencing an instance of an object, it establishes a strong reference to that object. When the local variable falls out of scope, it releases its strong reference. And if that was the last strong reference to the object, that object will be deallocated.
func runTasks() {
var person = Person(name: "Reuben")
// At this point, the `Person` object has one strong reference, as does the `MacBook`
doSomething(with: person)
// The `person` variable falls out of scope, the sole strong reference to the `Person`
// instance will be relieved, and the `Person` object will therefore be deallocated.
}
Perhaps properties references aren't accessed/destroyed because they are part of the object?
When an object is deallocated, any of its properties that had their own strong references to something else are automatically released, too.
func runTasks() {
var person = Person(name: "Reuben")
var computer = Macbook(model: "Pro 2020")
// At this point, the `Person` object has one strong reference, as does the `MacBook`
person.macbook = computer
// Now the `Macbook` instance has two strong references, the local variable and the `Person`
doSomething(with: person)
// When `person` and `computer` variables the local variables fall out of scope
// at the end of this function, their respective strong references to the `Person`
// and the `Macbook` objects will be released. But since there are no more strong
// references to `Person`, it will be deallocated. But when it is deallocated,
// its strong reference to `Macbook` will be released automatically, too. So now,
// the two strong references to that `Macbook` object are now released, too (both
// the `computer` local variable and the `macbook` property of `Person`), so it will
// deallocated, too.
}
Now, as you have identified, a strong reference cycle (previously called a “retain cycle”) is when two or more objects are keeping strong references to each other (thus, unless you manually nil
one or more of those references, they will end up with lingering strong references to each other and therefore neither will be deallocated until the cycle is broken).
It might seem appealing for the memory management system to identify and break these strong reference cycles for us at runtime, but that is computationally impractical. It would have to build a memory graph, identifying which objects are referencing each other, and somehow figure out which reference to break. There are even scenarios where we might have some short-lived cycle that we absolutely would not want the OS to resolve for us (e.g. you create a dispatch queue, dispatch some blocks of work, and we rely on the queue to not be released until the dispatched blocks are finished; URLSession
is another example).
Fortunately, while this sort of constant memory graph analysis is not feasible at runtime, Xcode does offer a debugging tool to help identify these scenarios, namely, the “debug memory graph” feature. For more information, see How to debug memory leaks when Leaks instrument does not show them? or How can identify strong reference cycles in Swift? or iOS app with ARC, find who is owner of an object.
Fortunately, while we have wonderful diagnostic tools to find these strong reference cycles, preventing them in the first place is very easy, by breaking the cycles with weak
or unowned
references. The result is a highly performant, very simple memory management infrastructure, where cycles are easily avoided with weak
/unowned
, but we have some excellent debugging tools for identifying issues, when needed.