iOS interview preparation(part 1)

Dmitrii Trifononov
5 min readSep 21, 2023

--

I believe that many developers, regardless of their experience level, encounter similar challenges when preparing for technical interviews. It’s common to forget certain core concepts because daily work doesn’t always align with the topics covered in interviews. To address this, I’ve made the decision to explore fundamental interview questions and delve deeper into understanding the underlying principles.

Let’s kick things off with the question: “What is memory?”

Memory, essentially, consists of binary digits, or bits (combinations of zeros and ones), which collectively store and organize information. Memory can be categorized into three main types:

1. Stack: This is where all local variables reside. The stack is well-suited for managing short-lived variables because clearing them is as simple as moving the stack pointer. It’s particularly convenient when we call a function, and after it finishes, we need to release all the memory it used. However, the stack has limitations; for example, it requires knowing the size of objects during compile time. Each thread has its own stack, providing exclusive access to it. Objects with a known size during compile time are typically placed on the stack. Memory is allocated in the stack when a function is called and cleared when the function exits.

2. Heap: The heap is the home for dynamic objects. Heap storage is convenient for objects with a longer lifespan. Unlike the stack, each application has only one heap, and access can be shared among multiple threads.

3. Global Data: This is where static variables and constants are stored.

Additionally, there’s the Text segment, which contains machine code. It’s static and can only be accessed for reading purposes.

In Swift, objects that involve reference counting are stored on the heap. It’s crucial to properly manage them to avoid memory issues that could lead to app crashes. Fortunately, Swift offers a mechanism called Automatic Reference Counting (ARC) to handle this automatically, relieving us from the burden of manual memory management.

Swift distinguishes between value and reference types:

  • Reference types include classes, functions, closures, and actors. These are typically stored on the heap.
  • Value types encompass structs, enums, Ints, Strings, and more. They are usually stored on the stack.

However, there are exceptions where reference types can be stored on the stack. This occurs when the compiler can determine the size or lifetime of an object during compile time.

Surprisingly, value types can be stored on the stack in several scenarios:

  1. When using a protocol and it’s unclear whether a value or reference type will be used.
  2. When mixing value and reference types within the same context.
  3. When variables are captured in a closure. However, there’s still a possibility that these variables will remain on the stack if they aren’t modified within the closure.
  4. Value types with generic type parameters.

But how does this mechanism work?

Automatic Reference Counting (ARC) operates during compile-time by inserting retain and release operations at appropriate locations. However, reference counting itself occurs at runtime.

Deep dive into ARC

In Swift, each object allocated in the heap is wrapped within a HeapObject, which contains crucial information like reference counts and metadata.

There are three types of references in Swift:

  1. Strong: This reference type is used to keep objects alive. When the strong reference count drops to zero, the object is deallocated. Strong references increase the counter by one.
  2. Weak: Weak references address the issue of retaining cycles and extending an object’s lifetime without increasing its reference count. When there are no strong references to an object, a weak reference returns nil. Under the hood, weak references are implemented as a generic enum with two cases: “some(Value)” and “none.” They use a side table to safely reference the object.
  3. Unowned: Unowned references behave similarly to weak references, but they don’t return nil if the object loses all references. Instead, they lead to a crash as an attempt is made to access “dirty” memory. Unowned references can be useful for debugging and slightly improve app performance. However, it’s not recommended to use them solely for performance optimization. Unlike weak references, unowned references don’t rely on a side table but have a direct pointer to the memory.

Understanding these reference types is essential for effective memory management in Swift.

In my previous discussion, I mentioned the term “side-table,” and you might be curious about what it is and how it works.

When we create a weak reference to an object, a side table is generated. Instead of tracking the strong reference count directly, we start keeping a reference to this side table. The side table, in turn, maintains a reference to the object itself. This indirect approach to reference counting allows Swift to efficiently manage weak references and break potential retain cycles.

Here is how this class looks

class HeapObjectSideTableEntry {
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
}

Let’s explore how objects in Swift freed

In the Live state, an object is alive with reference counts set to 1. If there is a weak reference, a side table is created.

When the strong reference count (RC) reaches zero, the `deinit()` function is called, and the object transitions to the Deiniting state. At this stage, strong reference operations don’t work, and reading through an unowned reference triggers an assertion failure. New unowned references can still be added. If there’s a side table, weak operations return nil. From this state, two transitions are possible:

1. If there’s no side table (meaning no weak references) and no unowned references, the object transitions to the Dead state and is immediately removed from memory.

2. If there are unowned or weak references, the object enters the Deinited state. In this state, the `deinit()` function has been completed, and strong or weak reference operations are impossible. Attempting to read an unowned reference triggers an assertion failure. Two outcomes are possible from this state:

3. If there are no weak references, the object goes directly to the Dead state as described above.

4. If there are weak references and thus a side table, the object transitions to the Freed state. In the Freed state, the object is completely released and no longer occupies memory, but its side table remains alive.

Once the weak reference count reaches zero, the side table is also removed, freeing memory, and the object transitions to the final state: Dead. In the Dead state, nothing remains of the object except a pointer to it, which is released from the heap, leaving no traces of the object in memory.

Final words

I hope you enjoyed this article and got to know something new.
Will you in the next articles

--

--