ARC Explained
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 = myAgemyAge = 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 = myAgemyAge.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 🙂