17 min read

In this article by Andrew Wagner, author of the book Learning Swift – Second Edition, we will see how to manage memory. As we know that when using an app, not much is worse than it being slow and unresponsive. Computer users have come to expect every piece of software to respond immediately to every interaction. Even the most feature-rich app will be ruined if it is unpleasant to use, because it won’t manage the device’s resources effectively. Also, with the growing popularity of mobile computers and devices, it is more important than ever to write software that efficiently uses battery power. One of the aspects of writing software that has the largest impact on both responsiveness and battery power is memory management.

(For more resources related to this topic, see here.)

In this article, we will discuss techniques specific to Swift that allow us to manage memoryin order to ensure that our code remains responsive and minimizes its effect on battery life. We will do so by covering:

  • Value types versus reference types
  • Automatic reference counting

Value types versus reference types

All variables and constants in Swift are stored in memory. In fact, unless you explicitly write data to the filesystem, everything you create is going to be in memory. In Swift, there are two different types of categories. These two categories are value types and reference types. The only way they differ is how they behave when they are assigned to new variables, passed into methods, or captured in closures. Essentially, they only differ when you try to assign a new variable or constant to the value of an existing variable or constant.

A value type is always copied when being assigned somewhere new, while a reference type is not. Before we look at what exactly this means in more detail, let’s go over how we determine whether a type is a value type or a reference type.

Determining between a value type or reference type

A value type is any type that is defined either as a structure or an enumeration, while all classes are reference types. This is easy to determine for your own custom types based on how you declared them. Beyond this, all of the built-in types for Swift, such as strings, arrays, and dictionaries, are value types. If you are ever uncertain, you can test any type you want in a playground to see whether its behavior is consistent with a value type or a reference type. The simplest behavior to check is what happens on assignment.

Behaviour on assignment

When a value type is reassigned, it is copied so that afterwards, each variable or constant holds a distinct value that can be changed independently. Let’s look at a simple example using a string:

var value1 = "Hello"
var value2 = value1
value1 += " World!"
print(value1) // "Hello World!"
print(value2) // "Hello"

As you can see, when value2 is set to value1, a copy is created. That makes it so that when we append “World!” to value1, value2 remains unchanged as “Hello”. We can visualize them as two completely separate entities:

On the other hand, let’s look at what happens with a reference type:

class Person {
   var name: String

   init(name: String) {
       self.name = name
   }
}
var reference1 = Person(name: "Kai")
var reference2 = reference1
reference1.name = "Naya"
print(reference1.name) // "Naya"
print(reference2.name) // "Naya"

As you can see, when we changed the name of reference1, reference2 was also changed. So why is this? As the name implies, reference types are simply references to an instance. When you assign a reference to another variable or constant, both are actually referring to the exact same instance. We can visualize this as two separate objects referencing the same instance:

In the real world, this would be like two kids sharing a toy. Both can play with the toy but if one breaks the toy, it is broken for both kids.

However, it is important to realize that if you assign a reference type to a new value, it does not change the value it was originally referencing:

reference2 = Person(name: "Kai")
print(reference1.name) // "Naya"
print(reference2.name) // "Kai"

As you can see, we assigned reference2 to an entirely different Person instance, so they can now be manipulated independently. We could then visualize this as two separate references to two separate instances:

This would be like buying a new toy for one of the kids.

This actually shows you that a reference type is really a special version of a value type. The difference is that a reference type is not itself an instance of any type. It is simply a reference to an instance. You can copy the reference so that you then have two variables referencing the same instance, or you can give a variable a completely new reference to a new instance. With reference types, there is an extra layer of indirection based on the sharing of instances between multiple variables.

Now that we know this, the simplest way to verify whether a type is a value type or a reference type is to check its behavior when it is being assigned. If the second value is changed when you modify the first value, it means that whatever type you are testing is a reference type.

Behavior on input

Another place where the behavior of a value type differs from that of a reference type is when passing them into functions and methods. However, the behavior is very simple to remember if you look at passing a variable or constant into a function as just another assignment. This means that when you pass a value type into a function, it is copied, while a reference type still shares the same value:

func setNameOfPerson(person: Person, var to name: String) {
   person.name = name
   name = "Other Name"
}

Here, we have defined a function that takes both a reference type,Person, and a value type,String. When we update the person’s data within the function, the person we passed in is also changed:

var person = Person(name: "Sarah")
var newName = "Jamison"
setNameOfPerson(person, to: newName)

print(person.name) // "Jamison"
print(newName) // "Jamison"

However, when we change the string within the function, the string passed in remains unchanged.

The place where things get a little more complicated is with in-out parameters. An in-out parameter is actually a reference to the passed-in instance. This means that it will treat a value type as if it were a reference type:

func updateString(inout string: String) {
   string = "Other String"
}

var someString = "Some String"
updateString(&someString)
print(someString) // "Other String"

As you can see, when we changed the in-out version of string within the function, it also changed the someString variable outside of the function just as if it were a reference type.

If we remember that a reference type is just a special version of a value type where the value is a reference, we can infer what will be possible with an in-out version of a reference type. When we define an in-out reference type, we actually have a reference to a reference; that reference is then the one that is pointing to a reference. We can visualize the difference between an in-out value type and an in-out reference type like this:

If we simply change the value of this variable, we will get the same behavior as if it were not an in-out parameter. However, by making it an in-out parameter, we can also change where the inner reference is referring to:

func updatePerson(inout insidePerson: Person) {
   insidePerson.name = "New Name"
   insidePerson = Person(name: "New Person")
}

var person2 = person
updatePerson(&person)
print(person.name) // "New Person"
print(person2.name) // "New Name"

We start out by creating a second reference—person2—to the same instance as the person variable that currently has the name Jamison from before. After that, we pass the original person variable into our updatePerson method and have this:

In this method, we first change the name of the existing person to a new name. We can see in the output that the name of person2 is also changed because both insidePerson inside the function and person2 are still referencing the same instance:

However, we then also assign insidePerson to a completely new instance of the Person reference type. This results in person and person2 outside of the function pointing at two completely different instances of Person, leaving the name of person2 to be New Name and updating the name of person to New Person:

Here, by defining insidePerson as an in-out parameter, we were able to change where the passed-in variable was referencing to. This can help us visualize all of the different options as one type pointing to another.

At any point, any of these arrows can be pointed to something new using an assignment, and the instance can always be accessed through the references.

Closure capture behaviour

The last behavior we have to worry about is when variables are captured within closures. Closures can actually use variables that were defined in the same scope as the closure itself:

var nameToPrint = "Kai"
var printName = {
   print(nameToPrint)
}
printName() // "Kai"

This is very different from the normal parameters that we have seen before. We don’t actually specify nameToPrint as a parameter, nor do we pass it in when calling the method. Instead, the closure is capturing the nameToPrint variable that is defined before it. These types of captures act similarly to in-out parameters in functions.

When a value type is captured, it can be changed, and this will change the original value as well:

var outsideName = "Kai"
var setName = {
   outsideName = "New Name"
}
print(outsideName) // "Kai"
setName()
print(outsideName) // "New Name"

As you can see, outsideName was changed after the closure was called. This is exactly like an in-out parameter.

When a reference type is captured, any changes will also be applied to the outside version of the variable:

var outsidePerson = Person(name: "Kai")
var setPersonName = {
   outsidePerson.name = "New Name"
}
print(outsidePerson.name) // "Kai"
setPersonName()
print(outsidePerson.name) // "New Name"

This is also exactly like an in-out parameter. The only place where capture behavior differs from an in-out parameter is that you cannot reassign the captured reference value to a new instance. If you try, the compiler will produce an error. This means that it is safe to treat captured variables as in-out parameters because the compiler will stop you in the only place where they differ.

The other part of closure capture that we need to keep in mind is that changing the captured value after the closure is defined will still affect the value within the closure. We could take advantage of this in order to use the printName closure we defined previously to print any name:

nameToPrint = "Kai"
printName() // Kai
nameToPrint = "New Name"
printName() // "New Name"

As you can see, we can change what printName prints out by changing the value of nameToPrint. This behavior is actually very hard to track down when it happens accidently, so it is usually a good idea to avoid capturing variables in closures whenever possible. In this case, we were taking advantage of the behavior, but more often than not, it will cause bugs. Here, it would be better to just pass what we want to print as an argument.

Another way to avoid this behavior is to use a feature called capture lists. With this, you can specify variables that you want to capture by copying them:

nameToPrint = "Original Name"
var printNameWithCapture = { [nameToPrint] in
 print(nameToPrint)
}
printNameWithCapture() // "Original Name"
nameToPrint = "New Name"
printNameWithCapture() // "Original Name"

A capture list is defined at the beginning of a closure before any parameters. It is a comma-separated list of all the variables being captured that we want to copy within square brackets. In this case, we requested that nameToPrint be copied so that when we change it later, it does not affect the value that is printed out.

Automatic reference counting

Now that we understand the different ways in which data is represented in Swift, we can look into how we can manage memory better. Every instance we create takes up memory. Naturally, it wouldn’t make sense to keep all data around forever. Swift needs to be able to free up memory to be used for other purposes once our program doesn’t need it anymore. This is the key to managing memory in our apps. We need to make sure that Swift can free up all of the memory that we no longer need as soon as possible.

The way that Swift knows it can free up memory is when the code can no longer access an instance. If there is no longer any variable or constant referencing an instance, it can be repurposed for another instance. This is called “freeing the memory” or “deleting the object”.

Since value types are always copied when they are reassigned or passed into functions, they can be immediately deleted once they go out of scope. We can look at a simple example to get the full picture:

func printSomething() {
   let something = "Hello World!"
   print(something)
}

Here we have a very simple function that prints out “Hello World!”. When printSomething is called, something is assigned to a new instance of String with the value “Hello World!”. After print is called, the function exits, and something is therefore no longer in scope. At that point, the memory being taken up by something can be freed.

While this is very simple, reference types are much more complex. At a high level, an instance of a reference type is deleted at the point when there are no longer any references to the instance in scope anymore. This is relatively straightforward to understand but it gets more complex in the details. The Swift feature that manages this is called automatic reference counting, or ARC for short.

Object relationships

The key to ARC is that every object has relationships with one or more variables. This can be extended to include the idea that all objects have a relationship with other objects. For example, a car object would contain objects for its four tires, engine, and so on. It would also have a relationship with its manufacturer, dealership, and owner. ARC uses these relationships to determine when an object can be deleted. In Swift, there are three different types of relationships: strong, weak, and unowned.

Strong

The first, and default, type of relationship is a strong relationship. It says that a variable requires that the instance it is referring to must always exist as long as the variable is still in scope. This is the only behavior available for value types. When an instance no longer has any strong relationships to it, it will be deleted.

A great example of this type of relationship is with a car, which must have a steering wheel:

class SteeringWheel {}

class Car {
   var steeringWheel: SteeringWheel

   init(steeringWheel: SteeringWheel) {
       self.steeringWheel = steeringWheel
   }
}

By default, the steeringWheel property has a strong relationship with the SteeringWheel instance it is initialized with. Conceptually, this means that the car itself has a strong relationship with the steering wheel. As long as the car exists, it must have a relationship with a steering wheel. Since steeringWheel is declared as a variable, we could change the steering wheel of the car, which would remove the old strong relationship and add a new one; however, a strong relationship will always exist.

If we were to create a new instance of Car and store it in a variable, that variable would have a strong relationship to the car:

let wheel = SteeringWheel()
let car = Car(steeringWheel: wheel)

Let’s break down all of the relationships in this code. First, we create the wheel constant and assign it to a new instance of SteeringWheel. This sets up a strong relationship from wheel to the new instance. We do the same thing with the car constant, but this time, we also pass in the wheel constant to the initializer. Now, not only does car have a strong relationship with the new Car instance, but the Car initializer also creates a strong relationship from the steeringWheel property and with the same instance as the wheel constant:

So what does this relationship graph mean for memory management? At this time, the Car instance has one strong relationship: the car constant, and the SteeringWheel instance has two strong relationships: the wheel constant and the steeringWheel property of the Car instance.

This means that the Car instance will be deleted as soon as the car constant goes out of scope. On the other hand, the SteeringWheel instance will only be deleted after both the wheel constant goes out of scope and the Car instance is deleted.

You can envision a strong reference counter on every instance in your program. Every time a strong relationship is set up to an instance, the counter goes up. Every time an object strongly referencing it gets deleted, the counter goes down. If that counter ever goes back to zero, the instance is deleted.

The other important thing to realize is that all relationships are only in one direction. Just because the Car instance has a strong relationship with the SteeringWheel instance does not mean the SteeringWheel instance has any relationship back. You could add your own relationship back by adding a car property to the SteeringWheel class, but you have to be careful when doing this, as we will see in the strong reference cycle section coming up.

Weak

The next type of relationship in Swift is a weak relationship. It allows one object to reference another without enforcing that it always exists. A weak relationship does not contribute to the reference counter of an instance, which means that the addition of a weak relationship does not increase the counter, nor does it decrease the counter when removed.

Since a weak relationship cannot guarantee that it will always exist, it must always be defined as an optional. A weak relationship is defined using the weak keyword before the variable declaration:

class SteeringWheel {
   weak var car: Car?
}

This allows a SteeringWheel to have a car assigned to it, without enforcing that the car never be deleted. The car initializer can then assign this backwards reference to itself:

class Car {
   var steeringWheel: SteeringWheel

   init(steeringWheel: SteeringWheel) {
       self.steeringWheel = steeringWheel
       self.steeringWheel.car = self
   }
}

If the car is ever deleted, the car property of SteeringWheel will automatically be set to null. This allows us to gracefully handle the scenario in which a weak relationship refers to an instance that has been deleted.

Unowned

The final type of relationship is an unowned relationship. This relationship is almost identical to a weak relationship. It also allows one object to reference another without contributing to the strong reference count. The only difference is that an unowned relationship does not need to be declared as optional, and it uses the unowned keyword instead of weak. It acts very similarly to an implicitly unwrapped optional. You can interact with an unowned relationship as if it were a strong relationship, but if the unowned instance has been deleted and you try to access it, your entire program will crash. This means that you should only use unowned relationships in scenarios where the unowned object will never actually be deleted while the primary object still exists.

You may ask then, “Why would we not always use a strong relationship instead?” The answer is that sometimes unowned or weak references are needed to break something called a strong reference cycle.

Summary

Memory management is often considered difficult to understand, but when you break it down, you can see that it is relatively straightforward. In this chapter, we saw that in Swift, there are value types and reference types, which are critical to understanding how we can reduce memory usage and eliminate memory leaks. Memory leaks are created when an object has a strong reference to itself through a third party, which is called a strong reference cycle. We must also be careful that we keep at least one strongreference to every object that we want to stay around, or we may lose them prematurely.

With practice, you will get better at both preventing and fixing memory problems. You will write streamlined apps that keep your users’ computers running smoothly.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here