Memory Management Part 2: Reference Cycles, Closures and Debugging

Yahya Saddiq
Dev Genius
Published in
10 min readJun 27, 2021

--

This article is based on my talks at CocoaHeads Berlin and NSLondon.

My son “Yusuf” once told me that elephants are capable to remember water sources for decades. Photo by Photos By Beks on Unsplash.

Welcome to the second part of this mini-series on Memory Management! This part carries on from Part 1: Regions, Structures and Leaks. I recommend you start with the first part of this article series to get the most out of it.

In this article, you’ll learn about:

  • Breaking reference cycle in classes and closures
  • Escaping & non-escaping closures
  • Delayed deallocation
  • Xcode memory debugger
  • Locating the leak 🕵🏽‍♂️

Follow up!

In the previous part; the book, author and editor were stuck in the heap memory region and their memory slots weren’t reusable.

Because book and author have a reference to each other and each instance keeps the other one alive, same as for book and editor.

And we made things worse by ending the scope, the stack list items will be deallocated and there are no more references to the initial objects.

This is a classic case of a reference cycle, which leads to a software bug known as a memory leak. With a memory leak, memory isn’t freed up even though its practical lifecycle has ended.

Breaking Reference Cycle

So, can you guess which class should we 🛠👷🏽‍♂️👷🏽‍♀️?

Exactly, the book class, because it’s not the owner class and it stores references to its owners in properties.

A book isn’t always assigned to an editor, so let’s model it as an optional type. And the book doesn’t own the editor, so it makes sense to make it a weak reference as well.

Weak means the reference in this variable will not take part in reference counting. When a reference isn’t weak, it’s called a strong reference, which is the default in Swift.

The great thing about using weak variables is that they automatically detect when the underlying object has gone away. This is why they are always declared with an optional type and a var. Because they become nil once the reference count reaches zero.

Unowned references is another way to break reference cycles, which behave much like weak in that they don’t change the object’s reference count. But Unlike weak references, they always expect to have a value — you can’t declare them as optional.

Think of it this way: A book cannot exist without an author. At the same time, a book does not “own” the author therefore the reference should be unowned.

Here we guarantee that a book always has an author, hence, the author is not declared as optional. And a book can have an editor at a certain time, hence, the editor is declared optional.

On the other hand, books is a variable, so it can be modified after initialization.

Voila! our code breaks the reference cycle with the weak and unowned keywords.

All the deinit methods run and they will be de-allocated.

Closures

Closure is simply a function with no name; you can assign it to a variable and can be passed around like any other value.

They are named “Closures” because they can “close over” the variables and constants within the closure’s scope.

This simply means that closure can access, store and manipulate the value of any variable or constant from the surrounding context.

The variables and constants used within the body of closure are said to have been captured by the closure.

There are two types of closure, non-escaping and escaping.

Non-escaping Closure

In Swift, closures are non-escaping by default and they are:

  • Non-storable.
  • Executed in scope.
  • Doesn’t cause a reference cycle.

No need to use [weak or unowned self] for non-escaping closure, if you don’t mind delayed deallocation.

Escaping closure

  • Storable
  • Can be passed to other closures.
  • Can be executed in the future.
  • Causes reference cycles.

🤔 How escaping closure could cause a reference cycle?

Swift is a safe language, closures extend the lifetime of any object they use to guarantee those objects are alive and valid.

This automatic safety is cool, but it’s the reason for the reference cycle if you extend the lifetime of an object that itself captures the closure.

let’s see that in an example!

Reference Cycle in Escaping Closures

For example, if you add a lazy property that computes the book’s description for the book class.

Note: a lazy property isn’t assigned until its first use and that self is only available after initialization.

When we call the book.description() in our scope, the book object creates a block of memory for the description() and holds its reference. At the same time description() strongly captures self through title and author which in turn increases the reference count for the book object.

Again we have a reference cycle, now between the book and the description(). At end of the scope, only the author’s deinit method will be called.

Breaking Closure Reference Cycle

To break the cycle, you’ll need to know about a language feature called capture list.

Capture list is a language feature to help you control exactly how a closure extends the lifetime of objects it refers to. They are a list of variables captured by a closure.

Since the closure shouldn’t exist after you release the book object from memory, self will never be nil, so we can change the strong reference to an unowned one using a capture list.

No more reference cycle, Swift will deallocate description object by the end of the scope.

[weak self]

There are certain times when you can’t capture self as an unowned reference, because self might become nil and the app will crash for reading a nil object.

[weak self] means that the closure will not extend the lifetime of self, but If the underlying object representing self goes away, it gets set to nil.

The app won’t crash if self goes away but does generate a “String interpolation” warning which you can fix by the strong-weak pattern.

Note: It’s wise and always recommended to use [weak self] instead of unowned.

When should I use [weak self]?

You don’t always need a weak self, Besher Al Maleh

Here’s a comprehensive flow chart by Besher Al Maleh to decide whether your closure is escaping and need [weak self] or not.

Reference Cycle — GCD

GCD doesn’t cause a reference cycle unless you store it for future execution.

Reference Cycle — Animation

UIView.animate() doesn’t cause a reference cycle unless you store a UIViewPropertyAnimator for future execution.

Reference Cycle — Timer

A timer is different. It can retain an object even if it’s not stored in a property.

As long as the timer repeats and self isn’t captured in it, the timer will prevent the referenced object from deallocating due to indefinitely delayed deallocation rather than a memory leak.

Keep in mind to invalidate when a timer is not needed and weakly capture self in it.

Delayed Deallocation

This is not a memory leak, it’s a side effect that happens in both escaping and non-escaping closures. This side effect leads to undesired behaviours.

As we said earlier, if you didn’t weakly capture self, closures capture strongly any variable or constant from the surrounding context. This means closures will prevent strongly captured objects from getting deallocated for as long as the closure body is alive.

Check this article to learn about the scenarios that can keep the closure body is alive.

BookShop

Finally, time to show reference cycles using the Xcode Memory Debugger. I’m going to use a demo app BookShop (download the Bookshop app from here) to show you memory visualization and leak.

The demo app is a simple two views and a table app. It contains a book data stub with the publishing house model I talked about in Part 1: Regions, Structures and Leaks.

Xcode Memory Debugger

Apple introduced the new visual memory debugger in Xcode 8. Run the Bookshop app and debug the memory graph.

The memory debugger breaks the execution of the app and builds a visualization of memory usage at the current time and makes it easier to locate, diagnose and fix memory leaks. Note that graph drawing might take a little while, depending on how big your app is.

The navigator window on the left shows the objects created on the heap organized by type and instance.

When you select an object in the navigator, the middle section will show you the memory graph of the selected object, and the relationships it has to other objects. The type of relationship is denoted by the style of the arrow. If the arrow is bold that’s a strong relation otherwise it’s a conservative relationship (🤔 Swift doesn’t recognize it, it could be weak or strong).

Sometimes, it’s a big graph, other times, it’s a smaller graph with just a couple of objects. The memory inspector on the right side shows details such as the size, class hierarchy and allocation backtrace.

Awesome! Memory doesn’t show any leaks so far.

Now, unpause the app and release the current view and objects by going back to the root view. Then open the memory graph again.

Oops! The book neither author objects have been released. This happens because the book is strongly holding the author and the author is strongly holding the book in an array list. That’s a reference cycle that caused memory leaks. The more bookshop view you open, the more leaks the memory will have.

As I have mentioned earlier, a memory leak is a memory that was allocated at some point but was never released and is no longer referenced by your app. Since there are no references to it, there’s no way to release it and that memory location cannot be used again.

Locating the Leaking Source 🕵🏽‍♂️

Debugging memory-related bugs can be time-consuming if you do not know where to start looking. Fortunately, Xcode includes options for identifying memory problems closer to when those problems happen.

Apple has Malloc library which provides debugging features to help tracking down:

  • Memory smashing bugs
  • Heap corruption
  • References to freed memory
  • Buffer overruns.

This feature is not enabled by default, you can enable these debugging options through a set of environment variables.

There’s a lot of Malloc environment variables, the one I care about now is Malloc Stack Logging, it allows malloc to remember the function call stack at the time of each allocation.

This is something that is enabled per scheme. When you are not debugging memory leaks you should turn off malloc stack logging. Because of its intensive recording process and some properties aren’t logged fully when stepping through with the debugger.

If you enabled malloc stack logging, the backtrace will show you the allocation line in your code for each object in the memory graph. Usually, the allocation lines are harmless and the problem is before or after that line.

As we mentioned in the diagram at the beginning of this article, to fix this memory leak you have to change the book-author relationship into an unowned one. A book cannot exist without an author, at the same time, a book does not “own” the author.

Where to Go From Here?

3 final tips to help you eliminate memory bugs:

  1. Have a strong code style defined for your project and respect it.
  2. Use Swift Lint. It is a great tool to detect early issues at compile-time.
  3. Profile the app frequently especially before creating a feature or a fix pull-request using Xcode Memory Debugger.

Awesome! you’ve reached the end of this mini-series.

In this article, you’ve learned how to break reference cycles in both classes and closures, Xcode memory debugger and how to locate leaks using Apple’s Malloc library.

References

Books
-
Swift Apprentice, Raywenderlich

Articles
-
Memory management and performance of value types, Swift Rocks
- Memory management in programming, Deepu Tech
- Memory Stack vs Heap, Gribble lab
- An illustrated guide to memory, stack, heap and pointer, Young Coder
- Xcode visual memory debugger, Use Your Loaf
- Memory leaks and retain cycle detection, DoorDash Engineering
- You don’t always need a weak self, Besher Al Maleh

I hope you enjoyed this article. If you have any question or comment, don’t hesitate, just drop them down here 🤗.

--

--

An accomplished programmer and dedicated educator, known for his expertise in software development. Often sharing his insights on tech and personal growth.