ARC in Swift — A Detailed Explanation
Intro
The memory management mechanism is different for every language. But it’s the very core of programming and building robust, feature-rich applications. In Java, we have GC (garbage collector), but in languages like Obj-c or C++, we developers are responsible for the allocation and deallocation of memory manually. But thanks to Apple, we have ARC in Swift which automatically tracks and manages the allocation and deallocation of objects.
In most cases, we don't have to do anything for ARC to work, but there can be cases where ARC will need our help.
How ARC (Automatic Reference Counting) works?
Structs and Enums are value types, whereas Classes are reference types. So reference counting only applies to Class Instances.
Value-type instances are typically stored directly on the stack or as part of another value, and their memory is automatically managed by the compiler. When a value type goes out of scope or is no longer needed, it is automatically deallocated without the need for reference counting or manual memory management.
Okay, Let’s take an example.
// Created a class
class SomeClass {
// Instance Property
var id: String
// Intializer
init(id: String) {
self.id = id
print("SomeClass with id \(id) has been initialized")
}
deinit {
print("SomeClass with id \(id) has been de-initialized")
}
}
var a: SomeClass? = SomeClass(id: "A") // Reference count of instance is now 1
// Prints "SomeClass with id A has been initialized"
var b: SomeClass? = a // Reference count inscraeses to 2
var c: SomeClass? = a // Reference count inscraeses to 3
We declared a class named SomeClass and created three references to the same instance. As they are all referring to the same instance the initializer will get called only once.
Now the instance is being referred by a, b, and c, so the reference count is 3.
a = nil // decreases the reference count by 1, remaining 2
b = nil // decreases the reference count by 1, remaining 1
c = nil // decreases the reference count by 1, remaining 0
// Prints "SomeClass with id A has been de-initialized"
Setting a, b, and c to nil will break the reference and upon reaching a reference count of 0, the instance will get de-initialized, thus calling the deinit.
Okay. So now we know how ARC works. But will this always be simple?
Let’s take another example.
class A {
var b: B?
deinit {
print("Deinitialized A")
}
}
class B {
var a: A?
deinit {
print("Deinitialized B")
}
}
var a: A? = A()
var b: B? = B()
a?.b = b
b?.a = a
a = nil
b = nil
If you run the above code, you won’t see any print statement. i.e. both the instance will not get deinitialized. Let’s assess what is happening here.
Class A & B both refer to each other and var a and b refer to respective instances.
So instances of class A and B are both having a reference count of 2.
When I set the var a to nil, instance A will not get de-initialized as instance B still holds a reference to instance A.
Setting b to nil won’t be able to de-initialize instance B as instance A which has not yet been de-initialized still holds a reference to instance B.
The above case is an example of Retain Cycle. Here both instances will never get de-allocated and thus will cause a memory leak and we don’t want that.
Resolving Retain Cycle
Well, not to worry ARC provides us with 3 types of reference with which we can solve Retain Cycle.
The three types of references are
- Strong (default)
- Weak
- Unowned
By default, references are strong which increases the ARC. Whereas weak & unowned will not increase the ARC.
The use of both weak and unowned can solve the Retain Cycle issue. But the selection of weak or unowned depends upon the relation and life cycle of both instances.
Weak Reference
A weak reference doesn’t keep a strong hold on the instance. A weak reference is always defined as an optional type as ARC automatically sets the reference to nil when the instance gets deallocated.
Property Observers don’t get called when ARC sets the value to nil.
class A {
var b: B? // A holds a strong reference to B
deinit {
print("Deinitialized A")
}
}
class B {
weak var a: A? // Now B holds a weak reference to A
deinit {
print("Deinitialized B")
}
}
var a: A? = A() // Initalizing A
var b: B? = B() // Initializing B
// setting the references
a?.b = b
b?.a = a
Here in this example, we made the reference to A inside B class a weak reference. Let’s visualize what's happening.
Instance B has 2 strong references, so the ARC for instance B is 2. But Instance A only has 1 strong reference so the ARC is 1.
a = nil // Sets the reference to nil will break the reference
// prints Deinitialized A
b = nil
// prints Deinitialized B
Setting a to nil breaks the only strong reference to instance A, so ARC will deallocate this instance. Now instance B is left with a single reference which is var b. So when we set it to nil, ARC will deallocate the instance B.
b = nil // Sets the reference to nil will break the reference
// still instance B has another strong reference which is
// from the instance A so deinit will not get called here
a = nil // This breaks the only reference to the instace A
// prints Deinitialized A
// Now instance B has no strong reference
// prints Deinitialized B
Changing the order in which we are setting the value of variables to nil, will not change the order of deallocation of the objects. In both scenarios, A will get deallocated first, and then B.
a = nil // prints Deinitialized A
print(b?.a) // Prints nil
b = nil // prints Deinitialized B
Here we can see when instance A gets de-initialized, the reference to this instance inside B is automatically updated to nil by ARC.
Unowned Reference
The’s two major differences between weak and unowned.
- Weak is always declared as optional, but there are no such constraints with unowned.
- For weak references, ARC automatically sets the reference to nil. But for unowned references, we are responsible to manage the instance references. Accessing an already de-initialized instance will cause a run-time exception.
Weak references are always used when the referred instance has a lesser lifetime and unowned is used when the referred instance has same or longer lif-time.
class A {
var b: B? // A holds a strong reference to B
deinit {
print("Deinitialized A")
}
}
class B {
unowned var a: A // Now B holds a unowned reference to A
init(a: A) {
self.a = a
}
deinit {
print("Deinitialized B")
}
}
var a: A? = A() // Initalizing A
var b: B? = B(a: a!) // Initializing B
// setting the references
a?.b = b
We changed the reference type to unowned.
b = nil
print(a?.b) // prints instance B
a = nil
// prints Deinitialized A
// prints Deinitialized B
The above code will work just fine. But what if we change the order?
When we set a to nil, instance A gets de-initialized. Now if we try to access the reference from instance B will get a run time exception. So whenever we are using unowned we have to be a little extra careful.
But unowned can have its own use cases where we are very sure of the instance lifetime and we want to avoid dealing with optional. Such use cases are seen with closure very often.
Closure And ARC
The retention cycle issue can also be caused between a closure and a class instance as closures are also reference types. Similar methods are applied to solve this issue. Closures use a capture list to specify the references.
class SomeClass {
var x: Int
lazy var displayValue: () -> Void = {
print("The instance has a value of ", self.x)
}
init(x: Int) {
self.x = x
}
deinit {
print("De initalized SomeClass")
}
}
var instance: SomeClass? = SomeClass(x: 6)
instance?.displayValue() // prints: The instance has a value of 6
instance = nil
Declaring displayValue as lazy lets us use
self
inside the closure. lazy property will not be accessed until after initialization has been completed andself
is known to exist.
See, in the above code SomeClass instance holds a strong reference to displayValue closure and the closure also holds a strong reference to the self, which is the current instance of the class.
To solve this issue we have to change the reference to self
as weak/unowned. Here, as the closure and class have the same lifetime unowned should be preferred.
lazy var displayValue: () -> Void = { [unowned self] in
print("The instance has a value of ", self.x)
}
Setting instance
to nil, will now de-allocate the instance calling the de-initializer.
instance = nil
// prints De initalized SomeClass
For the sake of example, we took the closure but for this particular use case class method could have been better.
func displayValue() {
print("The instance has a value of ", self.x)
}
We don't need to capture self
for methods and ARC works just fine as the method doesn't affect the strong reference count. But what if, we call a closure inside this function which has a longer lifetime than the class instance?
class SomeClass{
....
func displayValue() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
print("The instance has a value of ", self.x)
}
}
....
}
var instance: SomeClass? = SomeClass(x: 6)
instance?.displayValue()
instance = nil
The closure inside displayValue
has a strong reference toself
and is getting called after 2 sec. So even if we set instance = nil
, the de-initializer will get called when it’s finished execution of the closure, i.e
// After 2.0 sec
// prints The instance has a value of 6
// prints De initalized SomeClass
But if we use weak self?.x
inside the closure will return nil as the instance gets de-allocated and doesn’t wait for the closure to finish execution.
....
func displayValue() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
print("The instance has a value of ", self?.x)
}
}
....
instance?.displayValue()
instance = nil
// prints De initalized SomeClass
// After 2.0 sec
// prints: The instance has a value of nil
In contrast, if we use unowned, self.x
will throw a runtime exception as the instance gets de-allocated as soon as we set instance = nil
.
....
func displayValue() {
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { [unowned self] in
print("The instance has a value of ", self.x)
}
}
....
instance?.displayValue()
instance = nil
// prints De initalized SomeClass
// After 2.0 sec
//Thread 3: signal SIGABRT
Bonus Section
If a class Instance doesn't have any strong reference it’ll immediately get de-allocated.
autoreleasepool
For swift classes, ARC is able to properly manage the allocation and deallocation. But for pure Obj-c legacy classes sometimes can cause issues, like writing a loop that creates many temporary objects.
autoreleasepool block can solve this issue. The objects which get created inside this block will get de-allocated once the block completes execution.
In Obj-c autoreleasepool blocks are marked with @autoreleasepool
, and in Swift, we can use autoreleasepool
with a closure.
for i in 0...10000 {
autoreleasepool {
let file = "file_path_\(i)" // file path for ith image
let url = URL(fileURLWithPath: file)
let image = try! Data(contentsOf: url)
}
}