iOS – Why Is Advanced IOS Memory Management Valuable In Swift ?

Hello Readers, CoolMonkTechie heartily welcomes you in this article.

In this article, We will learn about what’s beyond the basics of iOS memory management, reference counting and object life cycle. Memory management is the core concept in any programming language. Memory management in iOS was initially non-ARC (Automatic Reference Counting), where we have to retain and release the objects. Now, it supports ARC and we don’t have to retain and release the objects. Xcode takes care of the job automatically in compile time. We will explain Memory management in swift from the compiler perspective. We will discuss the fundamentals and gradually make our way to the internals of ARC and Swift Runtime, answering below questions:

  • What is Memory Management?
  • What is Memory Management Issues?
  • What is Memory Management Rules ?
  • How Swift compiler implements Automatic Reference Counting?
  • How ARC Works ?
  • How to handle Memory in ARC ?
  • How strong, weak and unowned references are implemented?
  • What is Swift Runtime ?
  • What are Side Tables?
  • What is the life cycle of Swift objects?
  • What is Reference Count Invariants during Swift object lifecycle ?

A famous quote about learning is :

An investment in knowledge pays the best interest.”

So, Let’s begin.


What is Memory Management ?

At hardware level, memory is just a long list of bytes. We organized into three virtual parts:

  • Stack, where all local variables go.
  • Global data, where static variables, constants and type metadata go.
  • Heap, where all dynamically allocated objects go. Basically, everything that has a lifetime is stored here.

We’ll continue saying ‘objects’ and ‘dynamically allocated objects’ interchangeably. These are Swift reference types and some special cases of value types.

So We can define Memory Management :

Memory Management is the process of controlling program’s memory. It is critical to understand how it works, otherwise you are likely to run across random crashes and subtle bugs.”


What is Memory Management Issues?

As per Apple documentation, the two major issues in memory management are:

  • Freeing or overwriting data that is still in use. It causes memory corruption and typically results in your application crashing, or worse, corrupted user data.
  • Not freeing data that is no longer in use causes memory leaks. When allocated memory is not freed even though it is never going to be used again, it is known as memory leak. Leaks cause your application to use ever-increasing amounts of memory, which in turn may result in poor system performance or (in iOS) your application being terminated.


What is Memory Management Rules ?

Memory Management Rules are :

  • We own the objects we create, and we have to subsequently release them when they are no longer needed.
  • Use Retain to gain ownership of an object that you did not create. You have to release these objects too when they are not needed.
  • Don’t release the objects that you don’t own.


How Swift compiler implements automatic reference counting?

Memory management is tightly connected with the concept of OwnershipOwnership is the responsibility of some piece of code to eventually cause an object to be destroyed. Any language with a concept of destruction has a concept of ownership. In some languages, like C and non-ARC Objective-C, ownership is managed explicitly by programmers. In other languages, like C++ (in part), ownership is managed by the language. Even languages with implicit memory management still have libraries with concepts of ownership, because there are other program resources besides memory, and it is important to understand what code has the responsibility to release those resources.

Swift already has an ownership system, but it’s “under the covers”: it’s an implementation detail that programmers have little ability to influence.

Automatic reference counting (ARC) is Swift ownership system, which implicitly imposes a set of conventions for managing and transferring ownership.

Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

However, in a few cases ARC requires more information about the relationships between parts of your code in order to manage memory for you.

The name by which an object can be pointed is called a reference. Swift references have two levels of strength: strong and weak. Additionally, weak references have a flavour, called unowned.

The essence of Swift memory management is: Swift preserves an object if it is strongly referenced and deallocates it otherwise. The rest is just an implementation detail.”


How ARC Works ?

Every time you create a new instance of a class, ARC allocates a chunk of memory to store information about that instance. This memory holds information about the type of the instance, together with the values of any stored properties associated with that instance.

Additionally, when an instance is no longer needed, ARC frees up the memory used by that instance so that the memory can be used for other purposes instead. This ensures that class instances do not take up space in memory when they are no longer needed.

However, if ARC were to deallocate an instance that was still in use, it would no longer be possible to access that instance’s properties, or call that instance’s methods. Indeed, if you tried to access the instance, your app would most likely crash.

To make sure that instances don’t disappear while they are still needed, ARC tracks how many properties, constants, and variables are currently referring to each class instance. ARC will not deallocate an instance as long as at least one active reference to that instance still exists.

To make this possible, whenever you assign a class instance to a property, constant, or variable, that property, constant, or variable makes a strong reference to the instance. The reference is called a “strong” reference because it keeps a firm hold on that instance, and does not allow it to be deallocated for as long as that strong reference remains.

Example :

Here’s an example of how Automatic Reference Counting works. This example starts with a simple class called Person, which defines a stored constant property called name:

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

The Person class has an initializer that sets the instance’s name property and prints a message to indicate that initialization is underway. The Person class also has a deinitializer that prints a message when an instance of the class is deallocated.

The next code snippet defines three variables of type Person?, which are used to set up multiple references to a new Person instance in subsequent code snippets. Because these variables are of an optional type (Person?, not Person), they are automatically initialized with a value of nil, and do not currently reference a Person instance.

var reference1: Person?
var reference2: Person?
var reference3: Person?

You can now create a new Person instance and assign it to one of these three variables:

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

Note that the message "John Appleseed is being initialized" is printed at the point that you call the Person class’s initializer. This confirms that initialization has taken place.

Because the new Person instance has been assigned to the reference1 variable, there is now a strong reference from reference1 to the new Person instance. Because there is at least one strong reference, ARC makes sure that this Person is kept in memory and is not deallocated.

If you assign the same Person instance to two more variables, two more strong references to that instance are established:

reference2 = reference1
reference3 = reference1

There are now three strong references to this single Person instance.

If you break two of these strong references (including the original reference) by assigning nil to two of the variables, a single strong reference remains, and the Person instance is not deallocated:

reference1 = nil
reference2 = nil

ARC does not deallocate the Person instance until the third and final strong reference is broken, at which point it’s clear that you are no longer using the Person instance:

reference3 = nil
// Prints "John Appleseed is being deinitialized"


How to handle Memory in ARC ?

You don’t need to use release and retain in ARC. So, all the view controller’s objects will be released when the view controller is removed. Similarly, any object’s sub-objects will be released when they are released. Note that if other classes have a strong reference to an object of a class, then the whole class won’t be released. So, it is recommended to use weak properties for delegates.


How strong, weak and unowned references are implemented?

The purpose of a strong reference is to keep an object alive. Strong referencing might result in several non-trivial problems.

  • Retain cycles. Considering that Swift language is not cycle-collecting, a reference R to an object which holds a strong reference to the object R (possibly indirectly), results in a reference cycle. We must write lots of boilerplate code to explicitly break the cycle.
  • It is not always possible to make strong references valid immediately on object construction, e.g. with delegates.

Weak references address the problem of back references. An object can be destroyed if there are weak references pointing to it. A weak reference returns nil, when an object it points to is no longer alive. This is called zeroing.

Unowned references are different flavor of weak, designed for tight validity invariants. Unowned references are non-zeroing. When trying to read a non-existent object by an unowned reference, a program will crash with assertion error. They are useful to track down and fix consistency bugs.


What is Swift Runtime ?

The mechanism of ARC is implemented in a library called Swift Runtime. It implements such core features as the runtime type system, including dynamic casting, generics, and protocol conformance registration.

Swift Runtime represents every dynamically allocated object with HeapObject struct. It contains all the pieces of data which make up an object in Swift: reference counts and type metadata.

Internally every Swift object has three reference counts: one for each kind of reference. At the SIL generation phase, swiftc compiler inserts calls to the methods swift_retain() and swift_release(), wherever it’s appropriate. This is done by intercepting initialization and destruction of HeapObjects.

Compilation is one of the steps of Xcode Build System.


What are Side Tables?

Side Tables are mechanism for implementing Swift weak references.

Typically objects don’t have any weak references, hence it is wasteful to reserve space for weak reference count in every object. This information is stored externally in side tables, so that it can be allocated only when it’s really needed.

Instead of directly pointing to an object, weak reference points to the side table, which in its turn points to the object. This solves two problems:

  • saves memory for weak reference count, until an object really needs it.
  • allows to safely zero out weak reference, since it does not directly point to an object, and no longer a subject to race conditions.

Side table is just a reference count + a pointer to an object. They are declared in Swift Runtime as follows (C++ code).

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


What is the life cycle of Swift objects?

Swift objects have their own life cycle, represented by a finite state machine on the figure below. Square brackets indicate a condition that triggers transition from state to state. We will discuss the finite state machines in Eliminating Degenerate View Controller States.

In live state an object is alive. Its reference counts are initialized to 1 strong, 1 unowned and 1 weak (side table starts at +1). Strong and unowned reference access work normally. Once there is a weak reference to the object, the side table is created. The weak reference points to the side table instead of the object.

From the live state, the object moves into the deiniting state once strong reference count reaches zero. The deiniting state means that deinit() is in progress. At this point strong ref operations have no effect. Weak reference reads return nil, if there is an associated side table (otherwise there are no weak refs). Unowned reads trigger assertion failure. New unowned references can still be stored. From this state, the object can take two routes:

  • A shortcut in case there no weak, unowned references and the side table. The object transitions to the dead state and is removed from memory immediately.
  • Otherwise, the object moves to deinited state.

In the deinited state deinit() has been completed and the object has outstanding unowned references (at least the initial +1). Strong and weak stores and reads cannot happen at this point. Unowned stores also cannot happen. Unowned reads trigger assertion error. The object can take two routes from here:

  • In case there are no weak references, the object can be deallocated immediately. It transitions into the dead state.
  • Otherwise, there is still a side table to be removed and the object moves into the freed state.

In the freed state the object is fully deallocated, but its side table is still alive. During this phase the weak reference count reaches zero and the side table is destroyed. The object transitions into its final state.

In the dead state there is nothing left from the object, except for the pointer to it. The pointer to the HeapObject is freed from the Heap, leaving no traces of the object in memory.


What is Reference Count Invariants during Swift object lifecycle ?

During their life cycle, the objects maintain following invariants:

  • When the strong reference count becomes zero, the object is deinitedUnowned reference reads raise assertion errors, weak reference reads become nil.
  • The unowned reference count adds +1 to the strong one, which is decremented after object’s deinit completes.
  • The weak reference count adds +1 to the unowned reference count. It is decremented after the object is freed from memory.


Conclusion

In this article, We understood about Advanced iOS Memory management in Swift. Automatic reference counting is no magic and the better we understand how it works internally, the less our code is prone to memory management errors. Here are the key points to remember:

  • Weak references point to side a table. Unowned and strong references point to an object.
  • Automatic referencing count is implemented on the compiler level. The swiftc compiler inserts calls to release and retain wherever appropriate.
  • Swift objects are not destroyed immediately. Instead, they undergo 5 phases in their life cycle: live -> deiniting -> deinited -> freed -> dead.

Thanks for reading ! I hope you enjoyed and learned about the Advanced memory management concepts in Swift. Reading is one thing, but the only way to master it is to do it yourself.

Please follow and subscribe us on this blog and and support us in any way possible. Also like and share the article with others for spread valuable knowledge.

If you have any comments, questions, or think I missed something, feel free to leave them below in the comment box.

Thanks again Reading. HAPPY READING !!???

Loading

Leave a Comment