Foundation Set 4: Memory Management & ARC (20 Q&A)

Published:

๐Ÿ’พ Memory Management & ARC - 20 Questions

Memory management is critical for iOS interviews, especially senior positions. These questions cover the latest Swift 6 features and classic ARC concepts.


๐Ÿ”น ARC Fundamentals (Questions 1-5)

Q1: What is ARC (Automatic Reference Counting) and how does it work?

Answer:

ARC is Swiftโ€™s automatic memory management system that tracks and manages app memory by counting references to objects.

How it works:

  1. Strong reference created โ†’ Reference count +1
  2. Reference removed โ†’ Reference count -1
  3. Count reaches 0 โ†’ Object deallocated

Example:

class User {
    var name: String
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    deinit {
        print("\(name) deallocated")
    }
}

var user1: User? = User(name: "Alice")  // RC = 1
var user2 = user1                        // RC = 2
user1 = nil                              // RC = 1
user2 = nil                              // RC = 0 โ†’ deinit called

Key points:

  • โœ… Works for reference types (classes) only
  • โœ… Value types (struct, enum) donโ€™t need ARC
  • โœ… Compile-time feature - no runtime garbage collector
  • โœ… More predictable than GC (no pause times)

Interview insight: ARC is deterministic - objects deallocate immediately when RC hits 0, unlike garbage collection which runs periodically.


Q2: Whatโ€™s the difference between strong, weak, and unowned references?

Answer:

Reference TypeKeeps Object Alive?Becomes nil?When to Use
strongโœ… Yes (default)NoDefault for most properties
weakโŒ Noโœ… Yes (becomes nil)Delegates, parent references, optional relationships
unownedโŒ NoโŒ No (crashes if accessed)Non-optional relationships where lifetime is guaranteed

Code examples:

class Post {
    var title: String
    weak var author: User?  // weak - Post doesn't own User
    
    init(title: String) { self.title = title }
    deinit { print("Post deallocated") }
}

class User {
    var name: String
    unowned let account: Account  // unowned - User MUST have Account
    
    init(name: String, account: Account) {
        self.name = name
        self.account = account
    }
    deinit { print("User deallocated") }
}

class Account {
    var id: String
    init(id: String) { self.id = id }
    deinit { print("Account deallocated") }
}

// Usage
var account: Account? = Account(id: "123")
var user: User? = User(name: "Alice", account: account!)
// user holds UNOWNED reference to account
// If account deallocates first, accessing user.account crashes!

user = nil  // User deallocated
account = nil  // Account deallocated

When to use each:

strong:

class ViewController {
    let viewModel: ViewModel  // ViewController owns ViewModel
}

weak:

protocol TableViewDelegate: AnyObject { }

class TableView {
    weak var delegate: TableViewDelegate?  // Delegate pattern
}

unowned:

class Customer {
    let creditCard: CreditCard  // Every Customer has CreditCard
}

class CreditCard {
    unowned let customer: Customer  // Card can't exist without Customer
}

Interview tip: Use weak when reference can become nil, unowned when you GUARANTEE it wonโ€™t.


Q3: What is a retain cycle and how do you prevent it?

Answer:

A retain cycle occurs when two objects hold strong references to each other, preventing both from being deallocated.

Classic retain cycle:

class Person {
    var name: String
    var apartment: Apartment?  // Strong reference
    
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

class Apartment {
    var unit: String
    var tenant: Person?  // Strong reference
    
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deallocated") }
}

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A  // Person โ†’ Apartment (strong)
unit4A!.tenant = john     // Apartment โ†’ Person (strong)

john = nil
unit4A = nil
// โŒ MEMORY LEAK! Neither deallocates
// Person RC = 1 (held by Apartment)
// Apartment RC = 1 (held by Person)

Solution: Break the cycle with weak

class Apartment {
    var unit: String
    weak var tenant: Person?  // โœ… weak breaks the cycle
    
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deallocated") }
}

// Now:
john = nil  // Person RC = 0 โ†’ deallocates
// Apartment's weak reference to Person becomes nil automatically
unit4A = nil  // Apartment RC = 0 โ†’ deallocates
// โœ… Both deallocate properly!

Common retain cycle scenarios:

  1. Closures capturing self
  2. Delegate patterns
  3. Parent-child relationships
  4. Timer/notification observers

Q4: How do closures cause retain cycles?

Answer:

Closures capture and store references to variables they use. If a closure captures self strongly, it creates a retain cycle.

Problem:

class ViewController: UIViewController {
    var name = "HomeVC"
    var onComplete: (() -> Void)?
    
    func setupClosure() {
        onComplete = {
            print(self.name)  // โŒ Captures self STRONGLY
            // ViewController โ†’ closure (strong)
            // closure โ†’ ViewController (strong)
            // RETAIN CYCLE!
        }
    }
    
    deinit {
        print("\(name) deallocated")  // Never called!
    }
}

Solution 1: [weak self]

func setupClosure() {
    onComplete = { [weak self] in
        guard let self = self else { return }
        print(self.name)  // โœ… Captures self WEAKLY
    }
}
// Closure holds weak reference
// If ViewController deallocates, self becomes nil

Solution 2: [unowned self]

func setupClosure() {
    onComplete = { [unowned self] in
        print(self.name)  // โœ… Captures self as UNOWNED
    }
}
// Use when self CANNOT be nil when closure executes
// Crashes if self is deallocated - use carefully!

When to use each:

[weak self]: (Most common)

  • Completion handlers
  • Async operations
  • Callbacks where object might be deallocated

[unowned self]:

  • Closureโ€™s lifetime tied to self
  • Self CANNOT be nil when closure runs
  • Lazy properties

Real example:

class ImageDownloader {
    func downloadImage(url: URL, completion: @escaping (UIImage?) -> Void) {
        URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            guard let self = self else { return }
            // Process image...
            completion(image)
        }.resume()
    }
}

Q5: Whatโ€™s the difference between [weak self] and [unowned self]?

Answer:

[weak self][unowned self]
self becomes Optionalself stays non-optional
Becomes nil when deallocatedCrashes if accessed after deallocation
Requires guard let or if letUse directly like normal self
SaferFaster (no optional overhead)

Code comparison:

class DataManager {
    var data: [String] = []
    
    // WEAK - Safe, self might be nil
    func fetchDataWeak() {
        APIClient.fetch { [weak self] result in
            guard let self = self else { 
                print("DataManager deallocated")
                return 
            }
            self.data = result  // self is non-optional here
        }
    }
    
    // UNOWNED - Faster, but crashes if self is nil
    func fetchDataUnowned() {
        APIClient.fetch { [unowned self] result in
            self.data = result  // No guard needed, but crashes if deallocated!
        }
    }
}

When to use unowned:

class HTMLElement {
    let name: String
    let text: String?
    
    // Lazy closure will ONLY be called while HTMLElement exists
    lazy var asHTML: () -> String = { [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
}

Rule of thumb:

  • Default to [weak self] - itโ€™s safer
  • Use [unowned self] only when closureโ€™s lifetime is tied to objectโ€™s lifetime

Interview tip: Most engineers use [weak self] 95% of the time to avoid crashes.


๐Ÿ”น Swift 6 Ownership Model (Questions 6-10)

Q6: What is Swift 6โ€™s ownership model and how does it improve memory safety?

Answer:

Swift 6 introduces an ownership model that lets the compiler understand when values can be uniquely referenced, enabling new optimizations and safety guarantees.

Key features:

1. Move-only types

// Swift 6
struct FileHandle: ~Copyable {  // Can't be copied, only moved
    private let fd: Int32
    
    init(path: String) throws {
        fd = open(path, O_RDONLY)
    }
    
    consuming func close() {  // Consumes self (move semantics)
        Darwin.close(fd)
    }
    
    deinit {
        if fd >= 0 { Darwin.close(fd) }
    }
}

var file = try FileHandle(path: "/tmp/data")
// var file2 = file  // โŒ ERROR: Can't copy!
let file2 = consume file  // โœ… Moves ownership to file2
// file is now invalid

2. Stricter lifetime checks

// Compiler prevents use-after-free
func dangerousCode() {
    let data = SensitiveData()
    DispatchQueue.global().async {
        print(data.value)  // โŒ Compiler error!
        // data might be deallocated before async executes
    }
}

3. Optimized reference counting

// Swift 6 can eliminate retain/release when it knows object isn't shared
func processUser(_ user: borrowing User) {  // Borrow, don't copy
    print(user.name)
    // No retain/release needed!
}

Benefits:

  • โœ… Prevents entire classes of bugs at compile time
  • โœ… Performance improvements (fewer retain/release)
  • โœ… Clearer ownership semantics in code
  • โœ… Better interop with C++ and Rust

Interview insight: Swift 6โ€™s ownership model brings Rust-like memory safety without a separate borrow checker.


Q7: What are move-only types in Swift 6?

Answer:

Move-only types can transfer ownership but cannot be copied. Marked with ~Copyable.

Use cases:

// 1. File handles (can't duplicate OS resource)
struct FileDescriptor: ~Copyable {
    private let fd: Int32
    
    consuming func close() { /* ... */ }
}

// 2. Unique ownership (like Rust's Box)
struct UniqueBuffer<T>: ~Copyable {
    private let pointer: UnsafeMutablePointer<T>
    private let count: Int
    
    consuming func release() {
        pointer.deallocate()
    }
}

// 3. Lock guards
struct MutexGuard<T>: ~Copyable {
    private let mutex: Mutex<T>
    
    consuming func unlock() -> T {
        return mutex.unlock()
    }
}

How moves work:

var buffer = UniqueBuffer(count: 100)
let buffer2 = consume buffer  // Explicit move
// buffer is now INVALID - compiler prevents access

func processBuffer(_ b: consuming UniqueBuffer) {
    // Takes ownership of buffer
}

processBuffer(buffer2)
// buffer2 now invalid too

Benefits:

  • โœ… Compiler-enforced resource management
  • โœ… No accidental copies of expensive resources
  • โœ… Clear ownership transfer

Q8: Explain borrowing and consuming parameters in Swift 6

Answer:

Swift 6 adds explicit ownership annotations for function parameters.

Borrowing (default for most types):

// Function BORROWS the value (doesn't own it)
func printUser(_ user: borrowing User) {
    print(user.name)
    // user is still valid in caller after this
}

let user = User(name: "Alice")
printUser(user)
// user still valid here โœ…

Consuming (takes ownership):

// Function CONSUMES the value (takes ownership)
func processUser(_ user: consuming User) {
    // Do something with user
    // Caller can't use user after this
}

var user = User(name: "Bob")
processUser(user)
// user is now INVALID โŒ

Real-world example:

struct LargeDataset: ~Copyable {
    private var data: [Double]
    
    // Borrow for read-only operations
    func analyze() -> borrowing Statistics {
        // Don't copy anything, just compute
    }
    
    // Consume for transformations
    func transform(by fn: (Double) -> Double) -> consuming Self {
        data = data.map(fn)
        return self  // Move transformed dataset
    }
}

var dataset = LargeDataset(data: hugeArray)
dataset.analyze()  // Borrows, dataset still valid
dataset = dataset.transform { $0 * 2 }  // Consumes and returns new one

Interview tip: Similar to Rustโ€™s borrow checker but less strict and more Swift-friendly.


Q9: How do you detect memory leaks in iOS apps?

Answer:

Multiple techniques:

1. Instruments - Leaks Tool

Xcode โ†’ Product โ†’ Profile (โŒ˜I) โ†’ Leaks
  • Shows leaked objects in real-time
  • Call stack of allocation
  • Retain cycle visualization

2. Instruments - Allocations Tool

Product โ†’ Profile โ†’ Allocations
  • Track memory growth over time
  • Mark generation before/after action
  • See what objects persist

3. Debug Memory Graph (Best for retain cycles)

Debug โ†’ Debug Memory Graph (โŒฅโŒ˜M while running)
  • Visual representation of object relationships
  • Purple (!) marks potential retain cycles
  • Click object to see references

4. Malloc Stack Logging

// Enable in scheme:
// Edit Scheme โ†’ Diagnostics โ†’ Malloc Stack Logging

5. Deinit logging

class MyViewController: UIViewController {
    deinit {
        print("โœ… MyViewController deallocated")
    }
}
// If you never see this log, you have a leak!

6. Code review checklist:

// Check for:
[ ] Closures capturing self strongly
[ ] Delegates marked as weak
[ ] Timer/NotificationCenter observers removed
[ ] URLSessionDataTask properly canceled
[ ] Circular references in models

7. Automated testing:

func testViewControllerDoesNotLeak() {
    weak var weakVC: MyViewController?
    
    autoreleasepool {
        let vc = MyViewController()
        weakVC = vc
        // Use vc...
    }
    
    XCTAssertNil(weakVC, "ViewController leaked!")
}

Q10: What are common causes of memory leaks and how to fix them?

Answer:

1. Closures capturing self

// โŒ BAD
class DataManager {
    func loadData() {
        API.fetch { response in
            self.data = response  // Leak!
        }
    }
}

// โœ… FIXED
func loadData() {
    API.fetch { [weak self] response in
        self?.data = response
    }
}

2. Strong delegate references

// โŒ BAD
protocol MyDelegate {}
class MyView {
    var delegate: MyDelegate?  // Should be weak!
}

// โœ… FIXED
protocol MyDelegate: AnyObject {}  // Must be class-only
class MyView {
    weak var delegate: MyDelegate?
}

3. Timer retain cycles

// โŒ BAD
class ViewController {
    var timer: Timer?
    
    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.update()  // Timer holds strong ref to closure, closure holds strong ref to self
        }
    }
}

// โœ… FIXED
func startTimer() {
    timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
        self?.update()
    }
}

deinit {
    timer?.invalidate()  // Must invalidate!
}

4. NotificationCenter observers

// โŒ FORGOT TO REMOVE
class MyVC: UIViewController {
    override func viewDidLoad() {
        NotificationCenter.default.addObserver(
            self, 
            selector: #selector(handleNotif), 
            name: .myNotification, 
            object: nil
        )
    }
    // Forgot to remove observer! Leak!
}

// โœ… FIXED
deinit {
    NotificationCenter.default.removeObserver(self)
}

// โœ… OR use block-based API with weak self
notificationToken = NotificationCenter.default.addObserver(
    forName: .myNotification,
    object: nil,
    queue: .main
) { [weak self] notification in
    self?.handleNotif(notification)
}

5. URLSession data tasks

// โŒ Not canceling tasks
class ImageLoader {
    var task: URLSessionDataTask?
    
    func loadImage(url: URL) {
        task = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
            self?.image = UIImage(data: data!)
        }
        task?.resume()
    }
}

// โœ… FIXED
deinit {
    task?.cancel()  // Cancel ongoing task
}

๐Ÿ”น Quick Answer Cheat Sheet (Q11-20)

Q11: Value types vs ARC - Structs/enums donโ€™t use ARC, copied not referenced

Q12: Strong reference default - All references strong unless marked weak/unowned

Q13: Instruments Leaks tool - Profile app, track leaked objects

Q14: Memory warnings - Handle didReceiveMemoryWarning, clear caches

Q15: Autoreleasepool - Manage temporary objects in loops

Q16: Copy-on-write - Swift arrays copy only when modified

Q17: Implicit retain cycles - Lazy properties, computed properties can cause them

Q18: Weak vs unowned performance - Weak is slower (optional overhead)

Q19: Memory footprint - Value types on stack, reference types on heap

Q20: Swift 6 improvements - Ownership model, move semantics, better safety


๐ŸŽฏ Topics Covered

1-5: ARC Fundamentals (strong, weak, unowned, retain cycles)
6-10: Swift 6 Ownership Model (move-only, borrowing, consuming)
11-15: Memory leak detection and prevention
16-20: Advanced topics (autoreleasepool, COW, performance)

Difficulty: Medium-Hard
Time to Complete: 2-3 hours
Critical For: Senior iOS, Memory optimization roles
2025 Importance: Swift 6 ownership questions increasingly common!


๐Ÿ’ก Interview Tip: Be ready to draw memory diagrams showing object relationships and reference counts. Visual explanations impress interviewers!

๐Ÿ“ Next: Continue with Set 5: Concurrency to master async/await and actors!