Skip to content
advanced Phase 11 · Performance & Optimization

Memory Management

Understand ARC, retain cycles, weak references, and diagnose memory issues.

50m
3 problems
Topic Progress 0%

ARC Fundamentals

How ARC Works

Automatic Reference Counting (ARC) is Swift memory management system. Every time a new reference to an object is created, its reference count increments. When a reference goes out of scope or is set to nil, the count decrements. When the count reaches zero, the object is deallocated.

Strong References

By default, all references in Swift are strong:

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

var user1: User? = User(name: "Alice") // count = 1
var user2 = user1                       // count = 2
user1 = nil                             // count = 1
user2 = nil                             // count = 0, deallocated

Weak References

Weak references do not increase the reference count and are automatically set to nil when the object is deallocated:

class Apartment {
    let unit: String
    weak var tenant: User?  // Does not retain User
    init(unit: String) { self.unit = unit }
}

var alice: User? = User(name: "Alice")
var apt = Apartment(unit: "4A")
apt.tenant = alice        // Does not increase Alice reference count
alice = nil               // Alice is deallocated, apt.tenant becomes nil

Weak references must be declared as Optional since they can become nil.

Unowned References

Unowned references also do not increase the reference count but are not set to nil. Use them when the referenced object will never be nil during the reference lifetime:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

Unowned is faster than weak but will crash if the object is deallocated while the reference exists.

Retain Cycles & Weak References

Common Retain Cycles

Retain cycles occur when two objects hold strong references to each other:

class ViewController: UIViewController {
    var onComplete: (() -> Void)?
    override func viewDidLoad() {
        super.viewDidLoad()
        onComplete = {
            self.dismiss(animated: true) // self is strongly captured
        }
    }
}

Breaking Retain Cycles

// Solution 1: Capture list with weak self
onComplete = { [weak self] in
    self?.dismiss(animated: true)
}

// Solution 2: Capture list with unowned self
onComplete = { [unowned self] in
    self.dismiss(animated: true)
}

Delegate Retain Cycles

Delegates should always be weak to prevent retain cycles:

protocol DataManagerDelegate: AnyObject {
    func didUpdate(_ manager: DataManager)
}

class DataManager {
    weak var delegate: DataManagerDelegate?
    func update() { delegate?.didUpdate(self) }
}

NotificationCenter Retain Cycles

Observers can cause retain cycles. Use block-based observer with weak self:

class SettingsViewModel {
    private var observer: NSObjectProtocol?
    
    func setupObserver() {
        observer = NotificationCenter.default.addObserver(
            forName: .settingsChanged, object: nil, queue: .main
        ) { [weak self] _ in
            self?.handleChange()
        }
    }
    
    deinit {
        if let observer = observer {
            NotificationCenter.default.removeObserver(observer)
        }
    }
}

Memory Debugging

Memory Graph Debugger

Xcode built-in Memory Graph Debugger (Cmd+Shift+M) shows a visual graph of all live objects. Red indicators show likely retain cycles. Click on an object to see its reference graph and identify strong reference loops.

Using Instruments for Memory

The Allocations instrument tracks every allocation. Sort by Total Bytes to find the largest allocations. Check Persistent count for objects that should be deallocated. Use Mark Generation to compare memory between time points.

Common Memory Pitfalls

// Caching without eviction
class ImageCache {
    var cache: [String: UIImage] = [:]  // Grows forever
    func load(url: String) -> UIImage {
        if let cached = cache[url] { return cached }
        let image = downloadImage(url)
        cache[url] = image
        return image
    }
}

// Fix with NSCache or size limit
class ImageCache {
    private let cache = NSCache<NSString, UIImage>()
    func load(url: String) -> UIImage {
        if let cached = cache.object(forKey: url as NSString) {
            return cached
        }
        let image = downloadImage(url)
        cache.setObject(image, forKey: url as NSString)
        return image
    }
}

Debugging Use-After-Free

Use the Zombies instrument to detect accessing deallocated objects. Enable it in Instruments and run your app. The instrument flags every message sent to a deallocated object, showing where it was allocated and where it was freed.

Memory Best Practices

  • Use value types (struct, enum) when possible to avoid reference counting overhead
  • Prefer weak over unowned unless you are certain the reference will not outlive the object
  • Release large data structures when they are no longer needed
  • Use NSCache for image and data caching with automatic eviction
  • Profile with the Memory Graph Debugger regularly during development

Quiz

1. What happens when an object reference count reaches zero in ARC?

Question 1 options

2. When should you use weak vs unowned references?

Question 2 options

3. What causes a retain cycle in closures?

Question 3 options

4. How do you access the Memory Graph Debugger?

Question 4 options

Flashcards

Question

What is the difference between weak and unowned?

Answer

Weak references become nil when the object is deallocated and must be Optional. Unowned references do not become nil and will crash if accessed after deallocation.

Question

What is a retain cycle?

Answer

When two objects hold strong references to each other, preventing either from being deallocated. Broken with weak or unowned references.

Question

Why use NSCache instead of a Dictionary for caching?

Answer

NSCache automatically evicts objects under memory pressure. Dictionaries grow unbounded and can cause memory warnings.

Revision Notes

Key Takeaways

  • 1. ARC manages memory through reference counting
  • 2. Retain cycles prevent deallocation - use weak/unowned to break them
  • 3. Weak references must be Optional type
  • 4. Use Memory Graph Debugger to visualize object references
  • 5. Prefer value types to avoid reference counting overhead

Interview Tips

  • Explain the difference between strong, weak, and unowned
  • Describe how ARC works under the hood
  • Walk through finding and fixing a retain cycle
  • Discuss when to use NSCache vs Dictionary for caching

Cheat Sheet

Memory Management Quick Reference

  • Strong (default): increases reference count
  • Weak: does not increase count, becomes nil on dealloc
  • Unowned: does not increase count, crashes if accessed after dealloc
  • Use [weak self] in closures to prevent retain cycles
  • Delegates should always be weak
  • NSCache for automatic memory eviction