A Look Into Automatic Reference Counting in Swift

Manage your app’s memory usage efficiently

Neel Bakshi
Better Programming

--

A laptop and a cup of coffee lay on a table
Photo by Tomáš Stanislavský on Unsplash

Automatic Reference Counting (ARC) is a memory-management solution that makes sure the memory for the different objects and functions you create are properly allocated and deallocated so the device on which your code runs doesn’t run out of memory.

In order to understand ARC better, let’s give this discussion a little structure by understanding what we as programmers do.

We define variables and functions when we write code. These variables and functions need to be stored in the device’s memory so we can use them and they get removed from the memory once we don’t need them anymore — or, in other words, they need to have a lifetime. They come into existence when we want them to. Then, they perform their duties and die when they’re no longer being used so the memory they occupy can be freed up for new variables and functions to come into existence.

The creation part is easy. You usually init these functions and variables, but how do you decide when they should die?

There are two ways in which we can specify when these variables and functions should die:

  1. You, as a programmer, say when they’ve reached the end of their lifetime and need to die.

2. You let someone process manage it for you.

The first one is done using a process called reference counting, and the second one is called garbage collection.

We’re going to focus on the first one in this article.

What’s Reference Counting?

Reference counting is the process in which you maintain a count of strong references (more on this later, but for now just remember them to be strong references) to a particular instance.

So whenever you create an instance, you get an instance with a reference count of 1, which basically means that the variable pointing to this instance is saying, “Hey! I’m an owner of this instance, and until I no longer give it permission to get deallocated, it needs to stay alive”

So when you start having different strong variables point to it, its reference count increases by 1. When a reference is set to nil, the reference count of that instance of that reference type reduces by 1.

Note: As soon as the reference count becomes 0 the deinit for that particular instance is called.

You can always check the reference count of a variable by using the CFGetRetainCount function.

Note: Reference counting is done only to manage memory for reference types (e.g., classes and closures).

Background: Manual-Retain-Release Management

So in the previous section, we learned that there’s a count that’s maintained against the number of references there are to a particular instance.

Now the first observation is that in our daily life we don’t write code to increment any count, as such, whenever we create an instance (i.e., in its init) or decrement it when it’s set to nil. So that should bring us to the conclusion that the system must be managing all of this for us.

Although all of this is obsolete right now, it’ll help you understand why things work the way they do. You don’t have to memorize the following section completely, but do skim through it.

Anyway, let’s get back to our question: How does this happen?

To answer this question, we’ll need to go back a little in time when Objective-C was the only programming language for iOS/macOS code bases. Before ARC, there used to exist a thing called manual reference counting, where programmers had to write the “reference count” increment/decrement code themselves.

They did this using the following commands:

  • alloc init, new,copy: Creates a new object with a reference count of 1
  • retain: Increases the reference count by 1 and returns the object itself
  • release: Decreases the reference count by 1 and returns nil
  • autorelease: Marks this instance for autorelease when its pool is to be drained

In the manual reference count management–world, you, as a programmer, had the responsibility to make sure you’d implemented a dealloc method in every class that you created (for all you Swift devs, imagine writing a deinit function for every class you create).

Then, you had to make sure you called release on every instance variable of that class to make sure the reference count of those variables were reduced — otherwise, the memory occupied by those variables would never be freed and you’d end up with zombie objects (also known as a memory leak).

There were other boilerplate code that you had to handle, too. For example:

  • You’d have to implement setters for every variable that had to be changed. In the setter, you’d have to release the earlier value and set up this new variable.
  • If a particular variable was to be used within the scope of a function itself, then you’d have to release it just before the function ended — otherwise, there’d be a memory leak (unless the variable was marked autorelease, and your method was inside an autoreleasepool).

In order to make sure things ran smoothly, there were a set of rules that programmers had to adhere to in order to make sure your code was performant memory wise.

Rules for Manual-Release-Retain Management

You own the objects you create

If you call alloc init , new, or copy on any class or instance, you get a new instance with a reference count of 1, and now you have ownership of that particular instance.

Basically this is a convention used in the Obj-C world — even if you create an init function, you have to make sure all of the variables within that particular class instance are properly inited or retained.

You have to assume any function that doesn’t adhere to this naming convention should be an autorelease object; otherwise, it can release it whenever it wants, which might be unsafe.

Therefore, it’s OK if you put it in a autoreleasepool, or you can actually take ownership of it if you want to use it wherever you want using Rule 2 mentioned below so that even if the original class calls release on the object, you can keep using it safely.

You can take ownership of objects by using 'retain'

Let’s say you want to use a variable from one class, ClassA, in and instance of ClassB. You can ideally call retain on the variable to get ownership of that object.

You have to ‘release' objects you own

You should only call release on two types of objects.

  • You’ve either created an instance by using alloc init , copy, or new
  • You’ve actually called retain on a particular reference

You shouldn’t call ‘release’ on objects you don't own

This is a case that basically says you shouldn’t call release on objects retrieved from functions that don’t begin with alloc init , copy, or new (i.e., which properly follow the convention mentioned earlier in Rule 1) — for the sole reason that you don’t own them. If you want to manage ownership of them, you have to call retain on them.

Also a thing to note here is that in case you’re the one writing a function that returns an object without wanting to transfer ownership of that object, you need to alloc init that object and mark it as autorelease.

autorelease and @autoreleasepool

Like I mentioned earlier, there are a few conventions to be followed while doing manual-retain-release management, like making sure you’re calling release on objects you either alloc init or retain. Or by making sure that computed objects you return from methods that don’t start with alloc init, etc. are autoreleased and you don’t call retain on them since the caller isn’t going to be the owner of that returned instance.

Note: The basic definition of autorelease states that you’re marking an object that you’re creating but won’t be releasing (which goes against our original rules). You’re giving the responsibility or releasing that object to someone else.

@autoreleasepool is a block that on completion will release all of the objects that were created with an autorelease within it. Autorelease objects that were created outside the pool won’t be released.

Disadvantages of Manual-Retain-Release Management

As you can see from the above explanation, code written this way is riddled with conventions a programmer had to remember, and if they forgot to improperly retain , release or autorelease objects, they’d end up with two scenarios:

  1. Memory leaks — which happens when you call retain or create an object but forget to release. They continue to live forever until you actually close the application.
  2. Dangling Pointers — If you’d release objects you didn’t own, you would end up creating dangling pointers which, when accessed by someone else who is an actual owner, would result in a bad-access crash — EXC_BAD_ACCESS.

Automatic Reference Counting

So we now know that the entire count-increment/decrement process is a manual process that has severe consequences memory-wise if improperly managed, although another thing you would’ve noticed is that all of this was being done within a set of rules that every programmer had to abide by.

So one fine day, it was decided that this process of writing retain, release, andautorelease would be automated and it would be done during code compilation.

During compile time, the compiler would put in these calls at appropriate places based on a few “keywords” that’d identify how that ever reference type is to be referenced.

So if anyone asks you if ARC is a runtime or compile time process, you should definitely answer that it’s a mixture of both. The reference count “keywords” are put in during compile time but the actual counting and memory allocation/de-allocation takes place during runtime

Reference Types

In extremely basic terms, reference types (e.g., classes and closures) tell the compiler how the reference counts of the variable needs to be managed. There are three reference types that are present in Swift:

  1. strong: Increases the reference count by 1 whenever a new reference points to it — e.g., the compiler adds a retain call when you add a new variable point to a reference
  2. weak: Doesn’t increase the reference count
  3. unowned: Doesn’t increase the reference count

So when you specify variables with these identifiers, the compiler puts in the required retain and release calls in code before it actually compiles it to machine code.

'strong'

The strong keyword states that whenever a strong variable references a reference type, that instance’s reference count increases by 1.

This is the default reference type for any variable when nothing is specified.

Reference Cycles

Reference cycles occur when two strong variables point to each other. This causes the instances of both of these variables to remain alive even though they ideally shouldn’t. Let’s take an example:

In the above example you can see that after line 20, both a and b should’ve been de-allocated since we had made both of them nil , but internal references between each other created a reference cycle that prohibited the system from calling their deinit methods, thus creating a memory leak.

Now there are times when the reference count of an object never reaches zero; the only situation when this can happen in an ARC-enabled project is when there’s a reference cycle.

I've already shown one way of resolving the reference cycle here (lines 21-24). The other way of resolving a reference cycle is to make sure you can reference a variable without increasing its reference count, which brings us to our other keywords: weak and unowned.

'weak’ and 'unonwned'

Making one of the variables a weak (or unowned) variable fixes the reference-cycle problem.

This is because neither weak nor unowned increase the reference count of the object they’re pointing to, so naturally the question arises: Why do I need two keywords?

There’s a subtle difference between using weak and unowned, which is that weak variables are used to point to objects that needn’t be available for the lifetime of the holding object/original reference, whereas unowned variables are used to point to variables that have to be available for the full lifetime of the holding object/original reference.

Note: A weak variable is always optional, whereas an unowned variable can be optional (+ implicitly unwrapped optional) or a nonoptional.

Implementation-wise, it means that when the reference of a weak variable becomes nil, the weak variable itself becomes nil.

Whereas in the case of unowned, if the reference becomes nil, the unowned variable is still available but is pointing to a nil memory address, which can cause issues like a crash or unpredictable behavior.

unowned comes in two flavours:

  1. unonwed(safe): This reliably crashes the app when you try to use the unonwed variable referencing a deallocated variable
  2. unonwed(unsafe) : This doesn’t reliably crash the app when referencing a de-allocated variable and might cause unpredictable behavior. The reason I believe this is still present in Swift is probably because of the __unsafe_unretained ownership that still exists in LLVM. What this basically means is that you need to manually manage the memory for such variables since they’re not being manged by the compiler.

I’m going to clarify a few questions here that’ll make our understanding of both weak and unowned much better:

What’s the difference between 'weak' and 'unowned' optionals?

The answer isn’t exactly simple. Optionals basically mean that the variable can be nil. But in case of weak and unowned optional variables, there’s a subtle difference, which is shown in the following snippet.

Now if that variable b was weak instead of unowned, then there would be no crashes on either print statement.

This brings us to the explanation that weak optionals can be nil, and accessing them won’t cause any problems. On the other hand, unowned optionals can also be nil, but they can’t point to references that become nil.

When will you use 'unowned' over ‘weak’?

  • When you want a variable to be non-optional but don’t want a retain cycle
  • You want to make sure that the lifetimes of both the holder and the holdee match each other. For example:

In the above example, we’ve made the person variable unonwed because an ID needs to exist with a Person. If a Person instance exists, its ID must exist, but when the Person instance is de-allocated, the ID corresponding to that Person should also not exist anymore.

If you tried to access the Person for an ID when the Person object was de-allocated, your program would crash. You’d be able to deduce during your development phase itself that such a scenario was occurring and is in need of being fixed.

When would you use optional 'unowned’ variables?

When you want to avoid retain cycles and also want to make sure that the variable is set to nil properly when the reference it was pointing to has been removed so that the lifetimes of both the instances match each other.

What’s the difference between an implicitly unwrapped 'weak' optional variable and an optional 'unowned' variable given that both crash when they’re ‘nil’?

There’s again a subtle difference here. A nonoptional will always crash when accessed, but an optional unowned will crash only when its reference becomes nil and not when it is itself set to nil.

Reference Cycles in Closures

Closures in Swift are also reference types. Therefore, any reference type variables you use within a closure are strongly referenced by that particular closure, which can lead to a reference cycle if those variables reference this closure as well.

The answer to avoiding reference cycles in closures is a capture list. In these capture lists, you specify the variables you’ll be using as strong, weak, or unowned so they’re managed for ARC properly by the compiler.

The same rules for using the reference types apply over here as well. Ideally, you should try and avoid strong because that’ll create a reference cycle. You should instead use weak or unowned based on the lifetimes of the referenced variables. An example of capture lists:

Here’s another example to understand it better. If needed, copy and paste this piece of code into a playground, and apply breakpoints to run through the code to understand it better :

It’s worth mentioning here that capture lists work a little different for value types (even though this article is mostly about reference types).

If you capture a value type in a closure, a new copy of that variable is created, and any change done to the original value won’t be reflected in this new copy. But if you don’t capture the original variable and still use it within the closure (i.e., without specifying it in the capture list), then the compiler will actually hold a reference to it. Any changes to the original value will be reflected in the variable inside the closure as well.

How do you recognize Reference Cycles?

Given that we now know about reference cycles and how they effect each Value Types and Reference Types, it’s imperative that we know how to figure out reference cycles when looking at some code.

The trick — although confusing — is actually pretty simple. PLOT THE LINKS!

Let’s try a few examples to understand it better:

  1. A Simple Reference Cycle

2. Let’s look at an example of Reference Types containing closures:

3. Reference Cycles in Value Types 🤔

For more information read here

4. A much more real life example — where there is no reference cycle actually!

The answer to solving reference cycles in all the above examples is to capture the variables as weak or unonwed if they are reference types or capture the variable itself if they are value types.

Performance Implications of Using 'weak’ and 'unowned'

Are there any other reasons why one should be used instead of the other, except for just semantic reasons? Well there are some subtle performance benefits of using unowned when you know that the lifetime of the unowned variable ≤ lifetime of the original reference. The reason for that is the way weak references are managed.

The weak references are actually something called zeroing weak references. What that means is the runtime keeps track of all of the weak references you make to a reference (the original reference) using a table. When the original reference becomes nil, all these other weak references are made nil just before de-allocating it. This way, when you try to call something on the weak references once, the original reference will be set to nil. It’ll be safe and will behave according to whether it’s an optional or not.

In case of unowned, nothing of this sort needs to be done. For unowned(safe), maybe a check might be required to make sure it raises the exception, but in case of unowned(unsafe), nothing of that sort is needed since the behavior for this is anyway not predictable.

Therefore, we can see that there might be some kind of performance implications associated with using weak and unonwed properly.

--

--

Guy who handles everything mobile @headout among other things! Ex @practo