Better Programming

Advice for programmers.

Follow publication

Understanding iOS Memory Management With Toy Analogies

--

illustrations by author

I’ve been an iOS developer for some time now. I must confess that I’ve somewhat tiptoed around one particular topic all this time — memory management in iOS. A surprising admission, I know.

However, curiosity sparked me recently, so I decided to plunge headfirst into some research, bracing myself for the worst. But guess what? It wasn’t the monstrosity I once imagined to be. Far from it, in fact.

Memory management in iOS isn’t that complex after all. With the right analogies, I hope to make these concepts more approachable, and that is precisely what we’re going to explore in this article. From reference vs. value types, strong, weak, and unowned references, to retain cycles, memory leaks, Automatic Reference Counting (ARC), and optimization strategies, we’ll apply these concepts’ best practices to your apps!

Value Types vs. Reference Types

First, it is essential to know how instances work in Swift. It boils down to two categories.

Value types

The first category, value types, holds data exclusively within each instance. Whenever you assign a value type instance to another, its data is copied rather than shared.

To illustrate, let’s use a toy car analogy.

Suppose we first define a struct Car (a value type).

  1. We create a variable named car1 and initialize it as a new Car struct with the color "red."
  2. We create a second variable, car2and initialize it with car1.

The data in car1 is copied over to car2.

Now, let’s modify the car1.color property to be “blue.”

car1 color is now “blue” and car2 color is still “red.”

As you can see, each instance holds and fully owns its data and are 100% independent.

Reference Types

The second category, reference types, has data in memory accessed via references. This structure allows multiple instances to point to, and thus share, the same data. In Swift, a class is a perfect example of reference types.

To contrast, let’s now define a class Car (a reference type).

We create variables car1 and car2 similarly.

This time, when we change the car1.color property to "blue," both car1 and car2 change color because they are referencing the same object.

Different Reference Types

Now that we’ve learned the difference between value types and reference types, let’s delve into the different reference types in Swift: strong, weak, and unowned.

Strong reference

Let’s say Tommy is a Child who has a toy car. The toy car has a strong reference to Tommy as it ‘belongs’ to him. The toy car will be there as long as Tommy is around.

Tommy owns the toy car.

Tommy has a strong reference to the toy car.

Weak reference

Now, imagine Tommy has a friend named Susie.

Susie can play with Tommy’s toy car whenever she likes. Tommy generously lends it to her. However, the toy car is not Susie’s; it belongs to Tommy.

Susie borrows the toy car and plays with it, but she does not own the toy car.

Susie has a weak reference to the toy car.

If Tommy moves away or gets “deallocated” (in Swift terms), the toy car will also be gone.

Susie can’t play with the toy car anymore because it’s no longer there, but it’s okay because it was never hers to begin with.

Unowned reference

The third type of reference is ‘unowned,’ and it is slightly similar to a ‘weak’ reference but with a crucial difference.

Let’s consider a situation where Susie has an ‘unowned’ reference to Tommy’s toy car.

Like the weak reference, the toy car does not belong to Susie, but unlike a weak reference, the car is not optional. It’s always assumed to be there for Susie to play with.

In Swift, when Susie tries to play with the toy car while Tommy is away (or in other words, the toy car has been deallocated), it will lead to a program crash.

This is because an unowned reference presumes the object’s existence, and trying to access it when it’s not there will cause a runtime error.

Automatic Reference Counting (ARC)

Automatic Reference Counting, or ARC, is a system that keeps track of the number of owners an object has, much like who is currently playing with a particular toy.

Let’s go over another example.

Tommy, Susie, and Chuckie all go to daycare. The daycare has a toy box full of toys. Any child can take a toy out and play with it.

Tommy takes out a toy train. A new instance is created for the toy train (ARC = 1).

Susie wants to play with the toy train as well, so she joins Tommy. The toy train is now being played by two children (ARC = 2).

Chuckie sees all the fun his friends are having, so he decides to start playing with the toy train as well. (train’s ARC = 3).

In the end, Tommy gets bored and stops playing with the toy train (train’s ARC -1 -> ARC=2).

Susie then falls asleep and stops playing with the toy train as well (train’s ARC -1 -> ARC=1).

Finally, Chuckie is left with the toy train alone, but daycare is over, so he has to put it back in the toy box (deallocate), leaving us with no more children playing with the toy train (train’s ARC = 0).

Here’s a code example for the previous analogy:

Memory Leaks

Chuckie loves playing with the daycare’s toys. He takes a toy out of the toy box, plays with it, and when he is done, he puts it back.

One day, Chuckie decides putting the toy back in the toy box is too much work, so he decides to stop doing so.

He first takes out a toy robot.

He plays with it for a while, gets bored, and leaves it outside.

He then takes out a puzzle.

He plays with it, gets bored, and leaves it outside.

He then takes out a toy car.

Plays with it, gets bored, and leaves it outside.

This is fine for a while because the room has enough space (system memory).

But over time, as Chuckie keeps taking out more and more toys without putting them back, the room becomes cluttered. Eventually, so many toys are lying around that no one has room to play anymore.

This is an example of a memory leak!

In this code example, we don’t have a mechanism to put the toys back into the box. Even after Chuckie gets bored and stops playing with the toys, they’re still in memory because they’re stored in Chuckie's toys array.

This represents a memory leak. The toys remain in memory even when they're no longer needed.

Retain Cycles

Retain cycles are a type of memory leak.

Imagine Tommy and Chuckie have become best friends at daycare.

They make a pact that they won’t leave the daycare until the other one does too.

Chuckie has a strong reference to Tommy, and Tommy has a strong reference to Chuckie.

However, this creates a problem…

Even if their parents come to pick them up, they refuse to leave because each of them is waiting for the other one to leave first. Their strong references to each other prevent either of them from being able to leave the daycare.

This situation is an example of a retain cycle.

Optimization

After understanding the basics of memory management in iOS, let’s go over some tips for optimizing memory usage in your apps.

Choose appropriate types

  • Value types (structs, enums, tuples, and basic data types) are usually preferable for small, simple data structures or values that need to be copied rather than shared.
  • Reference types (classes) are better for larger, more complex data structures or when you need to share and modify instances.

Use weak and unowned references wisely

When you are dealing with reference cycles, you can use weak or unowned references to break them. However, they should be used judiciously:

  • Use a weak reference when the other instance has a shorter lifetime — that is, when the other instance can be deallocated first.

Example: Its best for Susie to have a weak reference to Tommy’s toy car knowing Tommy might go away anytime. This way Susie’s reference to the toy car will automatically become nil preventing any crashes caused by Susie trying to play with it while its no longer there.

  • Use an unowned reference when the other instance has the same lifetime or a longer lifetime.

Example: If Tommy is playing with a toy car that is owned by the daycare, Tommy might have an unowned reference to the toy car. This is because the toy car belongs to the daycare and will exist as long as or longer than Tommy’s time at the daycare. Since we know the toy car won’t be deallocated before Tommy’s time at the daycare ends, it’s safe to use an unowned reference here.

Lazy initialization

Swift provides a mechanism to create a property that is only created if and when it is accessed. If the property is never accessed, it is never created. This can save memory and improve performance.

Example: Toy is not initialized until Tommy starts playing with it.

Minimizing memory leaks

It’s important to ensure that all objects are properly deallocated when they’re no longer needed to avoid memory leaks. Leaks occur when you lose all references to an object but it’s still in memory because something else is holding onto it (like a closure or a delegate).

Tip: Tools like the “Leaks” instrument in Xcode can help identify these issues.

Example: Deallocate toys when Chuckie is done using them

Avoid retain cycles

Be mindful of reference cycles, especially in closures and delegate patterns. Use weak or unowned self within closures when necessary to avoid retain cycles.

Example: Make the child property bestFriend of weak reference type to avoid the Tommy and Chuckie leaving pact retain cycle.

Wrap Up

We’ve covered some foundational topics of iOS Memory Management. By understanding and applying these principles, you can significantly improve your app’s performance, reduce crashes caused by memory leaks, and retain cycles.

While this article covers the basics, I encourage you to delve deeper and learn more about how Swift and iOS handle memory!

It has been a while since I last posted, but I hope you’ve enjoyed and learned something. I plan on posting more frequently, so follow if you want to see more iOS development content like this!

--

--

No responses yet

Write a response