Friday, 6 October 2023

ARC — Automatic Reference Counting in Swift

 Hello everyone! I’m excited to share my knowledge about ARC (Automatic Reference Counting) and how it works in Swift. 

Automatic Reference Counting (ARC) is used in Swift to manage memory allocation and deallocation. ARC operates automatically, so you don’t need to worry about freeing up an instance from memory. However, it’s important to note that ARC may not catch all memory issues, and you can encounter memory leaks in your code due to mistakes that ARC doesn’t handle. We’ll discuss this topic in more detail later.

# How ARC Works

Every time you create a new instance of a class, ARC allocates a chunk of memory to store information about that instance. This memory holds information about the type of the instance, together with the values of any stored properties associated with that instance.

Additionally, when an instance is no longer needed, ARC frees up the memory used by that instance so that the memory can be used for other purposes instead. This ensures that class instances don’t take up space in memory when they’re no longer needed.

But how does ARC allocate and deallocate information from memory? Swift provides two methods for this: when we want to allocate a chunk of memory, we use the ‘init()’ method, and when we want to deallocate that data, we can use the ‘deinit()’ method.

class Person {
init() {
print("The person is allocated memory.")
}

deinit {
print("The person is freed up from memory")
}
}

var person: Person? = Person()
person = nil

// called both methods
// The person is allocated memory.
// The person is freed up from memory

We have one instance that holds a strong reference to the ‘Person’ class. If we don’t assign ‘nil’ to the ‘person’ instance, we can have a memory leak because the ‘deinit()’ method won’t be called.

We can encounter this issue when working with reference types like classes, closures, and so on. Swift provides us with the opportunity to resolve this issue by using the ‘weak’ or ‘unowned’ keywords.

Before passing to talk about “weak” and “unowned” let’s understand what is Reference cycles(Retain cycle).

# Reference cycles(Retain cycle)
It’s important to mention that strong references increase the retain count of an object, while weak and unowned references do not, which is why they’re used to break retain cycles.

We can encounter this issue when two classes refer to each other and create a retain cycle.

Retain Cycle 🔄
class Person {
var macBook: MacBook?

init() {
print("The person is allocated memory.")
}

deinit {
print("The person is freed up from memory")
}
}

class MacBook {

var person: Person?

init() {
print("The macBook is allocated memory.")
}

deinit {
print("The macBook is freed up from memory")
}
}

var person: Person? = Person()
var macBook: MacBook? = MacBook()

person?.macBook = macBook
macBook?.person = person

person = nil
macBook = nil

In the above example, a retain cycle occurs because we don’t call the ‘deinit()’ methods and fail to deallocate data from memory.

# How we can solve this issue?
First, let’s take the more challenging approach. We need to manually remember all relationships between classes and assign ‘nil’ to all properties to prevent memory leaks. You can add the code below and observe that ‘deinitialized(deinit)’ is called.

person?.macBook = nil
macBook?.person = nil

person = nil
macBook = nil

But this approach is too difficult to remember all properties and assign “nil” with chaining.

Let’s discuss when to use ‘weak’ and ‘unowned’ references in Swift.

First, I’d like to refactor the code to use “weak” and prevent memory leaks.

class Person {
var macBook: MacBook?

init() {
print("The person is allocated memory.")
}

deinit {
print("The person is freed up from memory")
}
}

class MacBook {

let macBookType: String
weak var person: Person?

init(typeOfMac: String) {
self.macBookType = typeOfMac
print("The macBook is allocated memory.")
}

deinit {
print("The macBook is freed up from memory")
}
}

var person: Person? = Person()
person?.macBook = MacBook(typeOfMac: "MacBook Pro 13")
print(person!.macBook!.macBookType)
person = nil
print(person?.macBook?.macBookType ?? "nil since macbook freed up from memory")

// output

// 1. The person is allocated memory.
// 2. The macBook is allocated memory.
// 3. MacBook Pro 13
// 4. The person is freed up from memory
// 5. The macBook is freed up from memory
// 6. 'nil' since macbook freed up from memory

We also can change “weak” to “unowned” and see image is the same.

Choosing Between “weak” and “unowned” References:

  • Use “weak” when there's a possibility that the referenced object can become “nil” (e.g., in delegate relationships).
  • Use “unowned” when you're sure that the referenced object will never become “nil” during the lifetime of the reference (e.g., parent-child relationships where the child always outlives the parent).

In summary, “weak” references are safe in situations where the referenced object can be deallocated, while “unowned” references are used when you're certain that the referenced object will always exist. Choosing between them depends on the specific needs of your code and the relationships between objects.

No comments:

Post a Comment