ARC Explained

Melvin John
6 min readMar 29, 2020

--

ARC, also know as Automatic Reference Counting, used to manage your app’s memory by keeping counts of all the allocated class instances and freeing up any allocated memory when those instances are no longer needed. Before we dive into ARC lets quickly discuss value types and reference types a they are an integral part to memory management.

Value Types

If you worked with Swift you should be able to distinguish value types with reference type. Struct and Enums are value types. Let’s see and example.

var myAge = 20
var yourAge = myAge
myAge = 30if myAge == yourAge {
print("Both are same")
} else {
print("Both are different")
}

// Both are different

You maybe surprised to see the above result because the changing the value of myAge did not change the value of yourAge which resulted in both properties being not equal. This is true because when you assign myAge to yourAge the value is copied rather than referenced.

Reference Types

Lets look at a reference type example

class Age: Equatable {

static func == (lhs: Age, rhs: Age) -> Bool {
lhs.value == rhs.value
}

var value = 20
}var myAge = Age()
var yourAge = myAge
myAge.value = 30if myAge == yourAge {
print("Both are same")
} else {
print("Both are different")
}
// Both are same

As you can see because AgeClass is a class type the myAge instance was passes by reference, so when we assigned myAge to yourAge, the value stored in myAge was not copied. Instead, the reference was assigned to yourAge. This means both myAge and yourAge are pointing to the same reference in memory.

ARC

Now that we know the differences of both value and reference types it’s important to understand that when it comes to memory management for value types, the compiler is good at disposing instances of structs and enums when they are no longer needed. However for reference types, it’s difficult for the compiler to know and understand when it’s safe to deallocate a class instance. This is where ARC comes in.

Every time you create a new instance of a class type, 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. When an instance is no longer needed, ARC frees up the memory used by that instance so that it can then be used for other purposes.

So whenever you assign a class instance to a property, constant, or variable, that property, constant, or variable makes a strong reference to the instance. The reference is called a strong reference because it keeps a firm hold on that instance and as long as that strong reference remains the objects are not allowed to be deallocated. To ensure this, ARC keeps a count called reference count and when an object’s reference count reaches zero the object is then deallocated. If you tried to access a deallocated instance, your app would most likely crash.

Let’s consider an example.

class Pet {
let name: String

init(name: String) {
self.name = name
print("Pet \(name) was initialised")
}
deinit {
print("Deallocating Pet \(name) ...")
}
}
class User {
let name: String
let pet: Pet
init(name: String, pet: Pet) {
self.name = name
self.pet = pet
print("User \(name) was initialised")
}
deinit {
print("Deallocating User \(name) ...")
}
}
func run() {
let pet = Pet(name: "Jacky")
let user = User(name: "Dave", pet: pet)
}
run()// Pet Jacky was initialised
// User Dave was initialised
// Deallocating User Dave ...
// Deallocating Pet Jacky ...

As you can see Pet and User objects were initialised and deallocated immediately, this is because the instances were in the run method allowing it to go out of scope, letting ARC deallocate it. If those instances were initialised globally then they will not be deallocated as those instances lives in the app/playground’s scope.

When a Pet instance is created there is a strong reference from the pet property to the new Pet instance, ARC marks the instance with reference count 1, same for the User instance, however when a User instance is created the reference count of property pet is now 2 as the user instance also holds on to pet. Naturally once the pet and user properties are out of scope they are deallocated by ARC and the allocated memories are freed, hence the deinit methods getting called.

Retain Cycle

As an app developer, you don’t usually have to worry about memory leaks since ARC takes care of deallocating unused objects, unfortunately this is not always the case as sometimes ARC require more information about relationships between instances in order to manage memory for you. If you don’t provide these information memory leaks can happen!

Let’s consider the same example as before but this time we are going to introduce an owner property to our Pet class, like below.

var owner: User?

We are then going to assign the user object we created in our run method to our pet object, like below.

per.owner = user

Run the above scenario and you should get the following print statements.

// Pet Jacky was initialised
// User Dave was initialised

As you can see the deallocating print statements are never called, which means the pet and user object that we created are not deallocated due to what we call a retain cycle.

Retain cycles are caused when an object A has a strong reference to an object B and object B has a strong reference to object A causing memory to leak. This fools ARC and prevents it from cleaning up. How can we solve this?

Weak Reference

To break retain cycles you could introduce a weak relationship between objects. As mentioned before ARC maintains a count of all the strong references, but with weak references the reference count of an object is never increased. So when a property, constant, or variable references another object and is marked as weak, the reference count of the object remain the same.

Let’s fix the above example by marking the pet property in the User class as weak. You could instead mark the owner property in pet as weak but i did not because a pet always has an owner whereas a user may not.

weak var pet: Pet?

A weak reference is always optional and automatically becomes nil when the referenced object is deallocated. That’s why you must define weak properties as optional var types for your code to compile

If you run the above scenario, you should get the following print statements.

// Pet Jacky was initialised
// User Dave was initialised
// Deallocating User Dave ...
// Deallocating Pet Jacky ...

Unowned Reference

This is another relationship type you could use that doesn’t increase the reference count. The difference between unowned and weak is that unowned references are never optional types. If you try to access an unowned property that refers to a deallocated object, you’ll trigger a runtime error similar to force unwrapping a nil optional type.

Unowned properties are defined like so.

unowned let pet: Pet

Retain Cycles With Closures

Like objects, closures are also reference types so if you are capturing references of objects for example self in a closure there’s a chance that you could run into a retain cycle.

Let’s introduce a lazy closure property in our User class.

lazy var completeInformation: () -> String = {
return "My name: \(self.name) and my pet's name: \(self.pet.name)"
}

This closure returns complete information about a user. The property is lazy because it’s using self.name and self.pet.name, which aren’t available until after the initialiser runs.

And finally let’s add the following line to the end of our run function.

print(user.completeInformation())

If you call the run function, you should get the following print statements.

// Pet Jacky was initialised
// User Dave was initialised
// My name: Dave and my pet's name: Jacky
// Deallocating Pet Jacky ...

As you can see the pet object was deallocated but the user object did not hence a memory leak due to a strong reference cycle between the closure and the user object.

We can solve this issue by capturing an unowned or weak reference to self in the closure like below.

lazy var completeInformation: () -> String = { [unowned self] in
return "My name: \(self.name) and my pet's name: \(self.pet.name)"
}

If you call the run function again you should see that the user object was deallocated.

Conclusion

ARC carries out most of the heavy lifting when it comes to memory management but it’s important to ensure that we provide enough information about the relationship between objects for it to work effectively. Because if you don’t, memory leaks can happen and they are usually hard to find.

I hope this article was useful and if i missed anything or you think there’s a better way, please let me know 🙂

--

--

Melvin John
Melvin John

Written by Melvin John

A Software Engineer with a passion for technology. Working as an iOS Developer @BBC

No responses yet