Back to blog

Automatic Reference Counting (ARC) in Swift


Aasif Khan
By Aasif Khan | Last Updated on July 6th, 2023 6:43 am | 5-min read

Automatic Reference Counting (ARC) is a mechanism to manage memory in Swift. Working with ARC concepts is essential in day-to-day iOS development. In this tutorial, we’ll discuss how ARC is used to manage memory on iOS.

Here’s what you’ll learn:

  • How Automatic Reference Counting works
  • Why memory management is important for iOS apps
  • How an object’s retain count affects deallocation
  • Practical examples of managing memory
  • Common pitfalls, like retain cycles
  • Working with strong and weak references

What’s Memory Management?

Every iPhone has a limited amount of RAM memory, currently around 4 GB. RAM is used for temporary storage of information when you’re using your iPhone. Data stored in RAM isn’t persisted between reboots; but reading from and writing to RAM is really fast.

  • RAM: Fast but small memory, the “short term memory” of a computer
  • Flash: Slow but large memory, the “long term memory” of a computer

All active apps on your iPhone, and the iOS operating system itself, share the available RAM memory. When one app uses a lot of memory, less of it is available to other apps. At the operating system level, iOS has processes that make memory use more efficient. We’re going to discuss how you can manage memory in your app, however.

When a new object is created with Swift, an amount of memory is allocated to accommodate for that new object. When an object isn’t needed anymore, it’s deallocated, which frees up space. Allocation and deallocation takes a non-trivial amount of CPU time and energy. You can compare this to a storage warehouse: it takes some effort to store a package.

Memory management is the process of finding what memory can be removed, so RAM is freed up. It’s a continual process: RAM is used for new data, and that data is removed when it’s not needed anymore. It’s a problem, of course, when the wrong data is removed from memory. As an app developer, your job is to write your app in such a way that memory can be managed (almost) automatically.

Unlike PC/Mac, iOS doesn’t use a swap file to write infrequently accessed or excess memory to persistent storage. Flash storage on an iPhone is simply not fast enough to be used as swap. Instead, when an iOS app uses more memory than the OS has allocated for it, it’ll ask the app to reduce its memory footprint or it kills the app.

Why Manage Memory?

Based on the idea that RAM memory is limited, and all apps share the same pool, an app needs to be a “good citizen” and manage memory efficiently. Hypothetically, if the Mail app uses 90% of memory, you wouldn’t be able to use Maps because you’d soon run out of memory.

When your iPhone would run out of memory, iOS kills apps that use too much. This degrades the user experience – you wouldn’t want that, as a developer!

On mobile platforms, Apple has a strong focus on long battery life. Keeping data active in RAM memory costs a considerable amount of energy. You can imagine that an app that uses too much memory, or more than necessary, strains battery life. When that app gets killed, it might need to write its state to persistent flash storage, which incurs another CPU and energy cost.

Why learn about memory management on iOS?

  1. It’s fun
  2. To avoid retain cycles

Simply said, RAM memory on an iPhone is a limited resource, and when used efficiently, the user experience on an iPhone is at its best. That’s why, as app developers, we’re managing memory.

ARC vs. Garbage Collection

On iOS, you use Automatic Reference Counting (ARC) to manage memory, which we’ll discuss next. On Android platforms, you use Garbage Collection (GC) to manage memory.

In short, Garbage Collection is a separate OS process that purges unused data from memory. This often comes at a performance cost, because the GC process has to figure out which memory to remove (“mark-and-sweep”).

ARC doesn’t have this bottleneck, because its memory management code is part of the app (as opposed to another process). A downside of ARC is that it takes more effort by the developer, and with ARC there’s always a risk of poorly managed memory and retain cycles.

How Automatic Reference Counting Works

Let’s discuss how Automatic Reference Counting (ARC) works. Let’s take a quick look at the essential principles of ARC.

  • The goal of Automatic Reference Counting is to know which data can be removed from RAM memory, to free up space.
  • An essential concept in ARC is the retain count, which is a number that keeps track of how many objects are “holding onto” to another object.
  • ARC only applies to reference types such as classes, and not to value types like structs. Value types are copied, so they don’t work with references.

You can already see where the name “Automatic Reference Counting” comes from.

  • It’s about references; the link between objects in your code.
  • It’s also about counting; keeping track of the number of retains of an object.
  • And it’s about automating; you want to spend the least amount of effort on memory management as a developer.

Up to 2011, when Automatic Reference Counting (ARC) was introduced in iOS 5 and Xcode 4.2, developers had to allocate and deallocate memory manually. You’d use alloc, retain and release to indicate which objects needed to be retained or released. The retain method would increase an object’s retain count, and release would decrease it. Just as with ARC, a retain count of zero meant the object was removed from memory. These days, ARC automates this process.

Keeping Track of the Retain Count

Every object in Swift – an instance of a class – has a property called retainCount.

  • When the retain count is greater than zero, the object is kept in memory
  • When this retain count reaches zero, the object is removed from memory

As we’ve discussed, only reference types, such as classes, have a retain count. A value type, such as a struct, is copied when it’s passed around in your code; there’s nothing to reference or retain. ARC only affects instances that are “shared” or passed-by-reference in your code.

Before we move on, let’s look at an analogy. Imagine a scenario where 3 persons – Berg, Pete and Sharon – share a piece of paper with a phone number written on it. We’re calling that piece of paper with the phone number paper. The paper is an instance of a Swift class, of course.

You can visualize this as 3 persons holding onto a piece of paper, grabbing it with their hands. It could be the phone number of a pizza place, for example. Berg, Pete and Sharon all need the phone number for something, so that’s why they’re holding onto that one shared piece of paper.

At this point, the retain count of paper is 3. Why? Because 3 persons are holding onto the paper object. This has a consequence: the paper object cannot be removed from memory, because it’s still in use.

At some point, Sharon doesn’t hold onto the paper with the phone number anymore. Pete also lets go of paper. They’re done calling the phone number, for example. The retain count is reduced to 2, and then to 1. Only Berg is holding on to paper. The data is not yet removed from memory.

Finally, Berg lets go of the piece of paper as well. You can imagine he just forgets he had it… This reduces the retain count of paper to zero, which promptly removes the paper object from memory. It’s not needed anymore, so the memory it used is freed up for other apps.

Let’s discuss how this works in practical Swift programming.

Technically, when you create an object of a particular class type, that object is called an instance, as in, “an instance of a class”. This is common terminology from Object-Oriented Programming principles. In modern day programming, and going forward, we often refer to “instances” as “objects”.

Automatic Reference Counting in Action

We’re going to take a look at ARC in action, so you can see how it works. First, we’ll create a simple class called Car. Like this:

class Car
{
var name = “”

init(name:String) {
self.name = name
print(“Instance of Car \(name) is initialized”)
}

deinit {
print(“Instance of Car is deinitialized”)
}
}

This Car class has one property name of type String. We’ve also defined two functions called init and deinit. These functions are called when an instance of Car is initialized and deinitialized. They’ll help us see when this happens.

Then, we’ll declare two variables:

var car_1:Car?
var car_2:Car?

Their type is Car?, so they’re optionals. Both variables have not yet been initialized.

Next, we’ll initialize car_1 by assigning it an instance of the Car class. We’ll also set its name property. Like this:

car_1 = Car(name: “Maserati”)

So far so good, right? We’ve just created a variable and assigned it a Car instance. Logically, the output of the code will be:

Instance of Car is initialized

Then, we’re going to assign car_1 to car_2. Like this:

car_2 = car_1

Both car_1 and car_2 now share a reference to the same Car instance. That’s how this works in Swift – we’ve copied the reference, but not the object itself. Both car_1 and car_2 now reference the same object!

Now, here’s where it gets interesting. We’re doing this:

car_1 = nil

The variable car_1 is set to nil, so the variable doesn’t hold a reference to the Car instance anymore. What’s the value of car_2 now?

The car_2 variable still refers to the Car instance, even though we’ve “nulled” the car_1 variable. The Car instance is retained in memory and not yet deallocated, because car_2 is still holding onto it.

Finally, we do this:

car_2 = nil

The output is now:

Instance of Car is initialized
Instance of Car is deinitialized

As you’ve guessed, this will deallocate the Car instance from memory because no variables are holding on to the instance anymore. Its retain count is zero, because the instance isn’t referenced anymore.

The core concept we’ve seen in practice here is the strong reference. In the example, both car_1 and car_2 keep a strong reference to the Car instance.

A strong reference will increase the retain count of an object when it’s referenced. As a result, the object will be retained in memory, even when other references are nulled, until there are no references anymore, after which the object is removed from memory. This is how ARC works.

Feel free to play around with the sandbox below. You can omit the last line, car_2 = nil, to see when the Car instance gets deallocated (or not).

class Car
{
var name = “”

init(name:String) {
self.name = name
print(“Instance of Car \(name) is initialized”)
}

deinit {
print(“Instance of Car \(name) is deinitialized”)
}
}

var car_1:Car?
var car_2:Car?

car_1 = Car(name: “Maserati”)

car_2 = car_1

car_1 = nil
car_2 = nil

How Allocation and Deallocation Works

In the previous example we’ve seen how a strong reference to an instance retains that instance in memory, because it’s retain count is greater than zero. Two references, car_1 and car_2 held onto the same Car instance until those references were erased by setting the variable(s) to nil.

We’ve also discussed how code like Car(), an initializer, will create an object and store that in memory. This is called allocation, because a piece of RAM memory is allocated to the storage of the Car object.

With that in mind, when exactly is the memory for Car deallocated? When is the memory space emptied, and freed up for other data? Moreover, in the previous example we explicitly set car_1 and car_2 to nil, which you typically don’t do in day-to-day coding, not for all objects at least.

What happens to a strong reference, if it’s not explicitly set to nil?

This question gets to the heart of Automatic Reference Counting. The good news is that it’s automatic – so you don’t have to think about it. The bad news is that because it’s automatic, to understand it, we’ll have to dig a little deeper.

Take a look at the following function:

func rentCar(type: String)
{
var rental = Car()
rental.type = type

// Drive car, return it, then do nothing
rental.drive()
}

In the above function, we’re creating a new rental car of a certain type. We then drive it, return it to the rental company, and go home. Now, ask yourself: When is the Car instance deallocated?

When you keep the retain count in mind, you can assert that the Car instance is deallocated at the end of the function rentCar(). Here’s why:

  • The retain count for the Car instance is increased from 0 to 1 when the rental variable references it
  • The type string is a value type, so it doesn’t affect the retain count of rental in any way
  • The drive() function, as far as we can see, doesn’t have any side-effects, so it doesn’t affect the retain count either
  • At the end of the function, the rental variable isn’t needed anymore – the scope of the function stops to exist – so rental is (implicitly) set to nil, and the Car object is released and deallocated from memory

No other code has created a strong reference to rental, and the instance doesn’t “escape” the rentCar() function; it’s not passed on. As a result, the Car instance is freed from memory.

What happens when rental would be a property of another class? That changes everything. Check this out:

class RentalCompany
{
var rental:Car?

func rentCar(type: String)
{
rental = Car()
rental.type = type

// Drive car, return it, then do nothing
rental.drive()
}
}

let company = RentalCompany()
company.rentCar()

By the end of the rentCar() function, the rental property still references the created Car object.

Its retain count is greater than zero, because an instance of RentalCompany is holding onto the Car instance referenced by the rental property. The strong reference is keeping the object in memory, because it can still be used by the RentalCompany class.

When is the Car instance released from memory? At the same point that the RentalCompany is deallocated. When the RentalCompany instance isn’t needed any more – its retain count is zero – the Car instance isn’t needed any more either, so they’ll both get removed from memory.

Common Problems with ARC

You see how keeping track of which object references others can get complicated. It’s super helpful that this is an (almost) automatic mechanism, thanks to ARC.

In the previous section, we’ve briefly discussed strong references. They hold onto instances that are referenced, by increasing their retain count. Strong references have a counter-part: weak references. Weak references do not increase an instance’s retain count.

Based on that, you can accidentally cause 2 common problems with ARC:

  1. A reference that’s weak when you need it to be strong, so the instance is removed from memory when you still need it later on
  2. A reference that’s strong when you need it to be weak, and that causes a retain cycle

Especially the retain cycle is a challenging problem. It’s quite simple: two objects hold onto each other with strong references, which means neither of them can be removed from memory because both their retain counts are greater than zero. This creates a memory leak, which is what we wanted to avoid in the first place by doing memory management and being a “good citizen”.

You can solve a retain cycle by designating either of the strong references as weak, but you’ll have to account for that change in your code too of course. Retain cycles also affect optionals, and for example [weak self] and unowned in closures. We’ll leave that for another tutorial!

Further Reading

Awesome! In this tutorial, you’ve learned more about memory management with Automatic Reference Counting (ARC) on iOS with Swift.

We’ve discussed that memory needs to be managed to know which data can be removed from memory, so storage can be freed up. You’ve also learned how ARC revolves around counting references and how that leads to the deallocation of instances. And finally, we’ve discussed a few problems that can arise from poorly managed memory.


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts