Memory management is a core concept in any programming language. Although there are many tutorials that explain the basics of Swift’s automatic reference counting, I found none that explain it from a compiler perspective. In this article, we’ll go beyond the basics of iOS memory management, reference counting, and object life cycles.

Let’s start with the basics and work our way inside the ARC and Swift Runtime, first thinking about the following:

  • What is memory?
  • How does the Swift compiler implement automatic reference counting?
  • How are strong, weak, and rudderless references implemented?
  • What is the life cycle of a Swift object?
  • What is a side table?

Memory management

On the hardware side, memory is just a long string of bytes. In virtual memory it is divided into three main sections:

  • The stack, where all the local variables are stored.
  • Global data, which contains static variables, constants, and type metadata.
  • The heap area, where all dynamically allocated objects reside. Basically, everything that has a life cycle is stored here.

We’ll continue to use “object” interchangeably with “dynamically allocated objects.” These are special cases of Swift reference types as well as value types.

Memory management is the process of controlling program memory. It’s important to understand how it works, or you might encounter random crashes and inexplicable bugs.

ARC

Memory management is closely related to the concept of ownership. Ownership determines which code causes the object to be destroyed [1].

Automatic Reference Counting (ARC) is part of Swift’s ownership system, which defines a set of conventions for managing and transferring ownership.

A variable alias that can point to an object is called a reference. Swift references have two levels of strength: strong and weak. In addition, weak references contain both masterless and weak references.

The essence of Swift memory management is this: If an object is strongly referenced, Swift keeps it, otherwise it frees it. All that remains are the implementation details.

Understand Strong, Weak and Unowned

The purpose of a strong reference is to keep an object alive. Strong references can cause several meaningful problems [2] :

  • A circular reference. Consider that Swift is not a strong reference to an object for cycle-collectingRIf it is also strongly referenced (possibly indirectly) by an object, it results in a circular reference. We had to write a lot of code to explicitly break the loop.
  • It is not always possible to make a strong reference immediately effective on object construction, such as delegates.

Weak references solve the problem of backreferences. If there is a weak reference to an object, the object can be destroyed. Weak references will return nil when accessing an object that no longer exists. This is called zeroing or zeroing.

A masterless reference is another form of a weak function, intended for strict validity invariants. A rudderless reference is non-zero. When you try to read an object that does not exist through a master reference, the program crashes with an assertion error. They are useful for tracking and fixing consistency issues.

class MyClass {	
    lazy var foo = { [weak self] in	
        // Must be validated	
        guard let self = self else { return }	
        self.doSomething()	
    }()	
    func doSomething() {}}Copy the code

A master reference does not need to be verified when used:

lazy var bar = { [unowned self] in	
  // No validation needed
  self.doSomething()	
}()	
Copy the code

In this example, it is wise to use a master reference because the property bar and self have the same lifetime.

Our further discussion of Swift memory management will be at a lower level of abstraction. We’ll delve into how ARC is implemented at the compiler level and the steps that each Swift object goes through before it is destroyed.

Swift Runtime

The ARC mechanism is declared in the Swift Runtime library. It includes core features such as a run-time type system, such as dynamic conversions, generic and protocol conformance registration [3]

The Swift Runtime uses the HeapObject structure to represent each dynamically allocated object. It contains all the data that makes up a Swift object: reference counts and type metadata.

Each Swift object in the HeapObject has three reference counts: one for each reference. During the SIL generation phase, the Swiftc compiler inserts the swift_retain() and swift_release() functions where appropriate. This is done by intercepting the initialization and destruction of the HeapObject.

Compilation is one of the steps in Xcode Build System

If you’re an old Objective-C programmer and want to know where the AutoRelease is, pure Swift objects don’t have it.

Now, let’s move on to weak references. The way they are implemented is closely related to the concept of Side tables.

To learn more about sideTables, read my previous article, Side Tables for Swift Weak Reference Management

Side Tables introduced

Side tables is central to implementing Swift weak references.

In most cases, objects do not have any “weak” references, so it is wasteful to reserve storage space for weak reference counts in each object. This information is stored in an external side table and is allocated only when it is really needed.

Rather than pointing directly to an object, a weak reference variable points to a side table, which in turn points to an object. This solves two problems: saving memory for weak reference counts that are not created until the object really needs them; Allows a weak reference to be safely zeroed out because it does not point directly to the object and is no longer the subject of the condition.

When two threads compete for the same resource, a race condition is said to exist if it is sensitive to the order in which the resource is accessed.

The Side table contains only a reference count and a pointer to an object. They are declared in the Swift Runtime as follows (C ++ code) [5] :

class HeapObjectSideTableEntry {
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
  // Operations to increment and decrement reference counts
}
Copy the code

Swift Life cycle of an object

Swift objects have their own life cycle, which I represent in the figure below as a finite state machine. The square brackets indicate the condition that triggers the state transition.

In the Live state, an object is active. Its reference count is initialized to strong: 1, unown: 1, and weak: 1 (the side table starts at +1). Once there is a weak reference to an object, the Side table is created. The weak reference points to the side table rather than the object.

Once the strong reference count reaches zero, the object moves from the Live state to the Deiniting state. Being in the Deiniting state means that deinit() is in progress. At this point, the strong reference operation is invalid. If there is an associated side table, access by weak reference will return nil. Access via unowned triggers an assertion failure. The new unowned reference can still be stored. From this state, two branches may be selected:

  • Quick judgment if there is no weak, the reference to unowned and the side table. The object is converted to Dead and is immediately removed from memory.
  • Otherwise, the object becomes Deinited.

In the Deinited state, deinit() has been executed and the object has an unfinished unown reference (at least the initial value: 1). At this point, storing and reading by strong and weak references cannot occur. Unowned reference storage does not occur either. Reading through Unown triggers an assertion error. The object can enter two branches from here:

  • If there is no weak reference, the object can be immediately released. It transitions to a Dead state.
  • Otherwise, there’s still oneside tableTo remove, and the object entersFreedState.

Before the Freed state, the object is fully Freed, but its side table is still active. At this stage, the weak reference count is set to 0 and the side table is destroyed. Object will be converted to its final state.

In the Dead state, all objects are destroyed except Pointers to objects. The pointer to “HeapObject” is also freed from the heap, and no trace of the object can be found in memory.

conclusion

Automatic reference counting isn’t magic, and the more we learn about it, the less prone our code will be to memory management errors. Here are some key points to keep in mind:

  • The weak reference pointer points to the Side table. Null and strong reference Pointers point to objects.
  • Automatic reference counting is implemented at the compiler level. The SwifTC compiler will be inserted in due courseswift_retain()swift_release().
  • Swift objects are not destroyed immediately. They go through five stages in their life cycle:live -> deiniting -> deinited -> freed -> dead

Vadim Bulavin 翻译 : music Coding

recommended

[1] : github.com/apple/swift…

[2] : github.com/apple/swift…

[3] : github.com/apple/swift…

[4] : github.com/apple/swift…

[HeapObject] : (github.com/apple/swift…

[6] : github.com/apple/swift…