iOS Advanced Memory Management: ARC, Weak References, and Memory Leaks

9 minute read

Published:

Memory management is crucial for iOS app performance and stability. While Swift’s Automatic Reference Counting (ARC) handles most memory management automatically, understanding weak references, retain cycles, and memory leaks is essential for building robust applications. This guide explores advanced memory management techniques with real, working code examples.

1. ARC Fundamentals and Reference Counting

import Foundation

// MARK: - Reference Counting Demonstration
class MemoryDemo {
    var name: String
    var referenceCount: Int = 0
    
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    
    deinit {
        print("\(name) deallocated")
    }
}

// MARK: - Reference Counting Examples
class ReferenceCountingExamples {
    func demonstrateReferenceCounting() {
        print("=== Reference Counting Demo ===")
        
        // Strong reference
        var object1: MemoryDemo? = MemoryDemo(name: "Object1")
        print("Object1 created")
        
        // Another strong reference
        var object2 = object1
        print("Object2 references Object1")
        
        // Remove first reference
        object1 = nil
        print("Object1 reference set to nil")
        
        // Remove second reference
        object2 = nil
        print("Object2 reference set to nil")
        // Object1 will be deallocated here
    }
}

// MARK: - Usage
let examples = ReferenceCountingExamples()
examples.demonstrateReferenceCounting()

2. Retain Cycles and Memory Leaks

// MARK: - Retain Cycle Example
class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    
    deinit {
        print("\(name) deallocated")
    }
}

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

// MARK: - Retain Cycle Demonstration
class RetainCycleDemo {
    func createRetainCycle() {
        print("=== Creating Retain Cycle ===")
        
        let john = Person(name: "John")
        let apartment = Apartment(unit: "4A")
        
        // Create retain cycle
        john.apartment = apartment
        apartment.tenant = john
        
        print("Retain cycle created - objects won't be deallocated")
        // Both objects remain in memory due to retain cycle
    }
    
    func breakRetainCycle() {
        print("=== Breaking Retain Cycle ===")
        
        let jane = Person(name: "Jane")
        let apartment = Apartment(unit: "5B")
        
        // Create retain cycle
        jane.apartment = apartment
        apartment.tenant = jane
        
        // Break the cycle by setting one reference to nil
        apartment.tenant = nil
        
        print("Retain cycle broken - objects will be deallocated")
        // Objects will be deallocated when they go out of scope
    }
}

3. Weak References and Unowned References

// MARK: - Weak Reference Implementation
class WeakReferenceExample {
    weak var weakReference: MemoryDemo?
    
    func demonstrateWeakReference() {
        print("=== Weak Reference Demo ===")
        
        let strongObject = MemoryDemo(name: "StrongObject")
        weakReference = strongObject
        
        print("Weak reference set")
        print("Weak reference is nil: \(weakReference == nil)")
        
        // Remove strong reference
        // strongObject goes out of scope here
        
        print("Strong reference removed")
        print("Weak reference is nil: \(weakReference == nil)")
    }
}

// MARK: - Unowned Reference Implementation
class CreditCard {
    let number: String
    unowned let customer: Customer
    
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
        print("Credit card \(number) initialized")
    }
    
    deinit {
        print("Credit card \(number) deallocated")
    }
}

class Customer {
    let name: String
    var creditCard: CreditCard?
    
    init(name: String) {
        self.name = name
        print("Customer \(name) initialized")
    }
    
    deinit {
        print("Customer \(name) deallocated")
    }
    
    func addCreditCard(number: String) {
        creditCard = CreditCard(number: number, customer: self)
    }
}

// MARK: - Unowned Reference Demo
class UnownedReferenceDemo {
    func demonstrateUnownedReference() {
        print("=== Unowned Reference Demo ===")
        
        let customer = Customer(name: "Alice")
        customer.addCreditCard(number: "1234-5678-9012-3456")
        
        print("Customer and credit card created")
        // Both objects will be deallocated when customer goes out of scope
    }
}

4. Closure Capture Lists

// MARK: - Closure Capture List Examples
class ClosureCaptureDemo {
    var name: String
    var completionHandler: (() -> Void)?
    
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    
    deinit {
        print("\(name) deallocated")
    }
    
    // MARK: - Retain Cycle in Closure
    func createRetainCycleInClosure() {
        print("=== Creating Retain Cycle in Closure ===")
        
        completionHandler = {
            print("Hello from \(self.name)")
        }
        
        // This creates a retain cycle because the closure captures self strongly
        print("Retain cycle created in closure")
    }
    
    // MARK: - Breaking Retain Cycle with Weak Self
    func breakRetainCycleWithWeakSelf() {
        print("=== Breaking Retain Cycle with Weak Self ===")
        
        completionHandler = { [weak self] in
            guard let self = self else {
                print("Self is nil")
                return
            }
            print("Hello from \(self.name)")
        }
        
        print("Retain cycle avoided with weak self")
    }
    
    // MARK: - Breaking Retain Cycle with Unowned Self
    func breakRetainCycleWithUnownedSelf() {
        print("=== Breaking Retain Cycle with Unowned Self ===")
        
        completionHandler = { [unowned self] in
            print("Hello from \(self.name)")
        }
        
        print("Retain cycle avoided with unowned self")
    }
    
    // MARK: - Multiple Capture List Items
    func demonstrateMultipleCaptureItems() {
        print("=== Multiple Capture List Items ===")
        
        let externalObject = MemoryDemo(name: "ExternalObject")
        
        completionHandler = { [weak self, weak externalObject] in
            guard let self = self else { return }
            print("Hello from \(self.name)")
            
            if let externalObject = externalObject {
                print("External object: \(externalObject.name)")
            }
        }
        
        print("Multiple weak references in capture list")
    }
}

5. Memory Management in SwiftUI

import SwiftUI
import Combine

// MARK: - SwiftUI Memory Management
class SwiftUIMemoryManager: ObservableObject {
    @Published var data: [String] = []
    private var cancellables = Set<AnyCancellable>()
    private var timer: Timer?
    
    init() {
        print("SwiftUIMemoryManager initialized")
        setupTimer()
    }
    
    deinit {
        print("SwiftUIMemoryManager deallocated")
        timer?.invalidate()
    }
    
    private func setupTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.updateData()
        }
    }
    
    private func updateData() {
        data.append("Item \(data.count + 1)")
    }
    
    func cleanup() {
        timer?.invalidate()
        timer = nil
        cancellables.removeAll()
    }
}

// MARK: - SwiftUI View with Memory Management
struct MemoryManagedView: View {
    @StateObject private var manager = SwiftUIMemoryManager()
    @State private var showDetail = false
    
    var body: some View {
        NavigationView {
            VStack {
                List(manager.data, id: \.self) { item in
                    Text(item)
                }
                
                Button("Show Detail") {
                    showDetail = true
                }
                .sheet(isPresented: $showDetail) {
                    DetailView(manager: manager)
                }
                
                Button("Cleanup") {
                    manager.cleanup()
                }
            }
            .navigationTitle("Memory Management")
        }
    }
}

struct DetailView: View {
    @ObservedObject var manager: SwiftUIMemoryManager
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Detail View")
                Text("Data count: \(manager.data.count)")
                
                Button("Dismiss") {
                    dismiss()
                }
            }
            .navigationTitle("Detail")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

6. Memory Leak Detection and Prevention

// MARK: - Memory Leak Detector
class MemoryLeakDetector {
    private static var trackedObjects: [String: WeakRef] = [:]
    
    static func track<T: AnyObject>(_ object: T, name: String) {
        trackedObjects[name] = WeakRef(object: object)
        print("Tracking \(name)")
    }
    
    static func checkForLeaks() {
        print("=== Memory Leak Check ===")
        
        for (name, weakRef) in trackedObjects {
            if weakRef.object == nil {
                print("✅ \(name) properly deallocated")
            } else {
                print("⚠️ Potential memory leak: \(name) still exists")
            }
        }
        
        // Clean up tracking
        trackedObjects.removeAll()
    }
}

class WeakRef {
    weak var object: AnyObject?
    
    init(object: AnyObject) {
        self.object = object
    }
}

// MARK: - Leak Detection Usage
class LeakDetectionDemo {
    func demonstrateLeakDetection() {
        print("=== Leak Detection Demo ===")
        
        // Create objects that might leak
        let leakyObject = LeakyClass(name: "LeakyObject")
        let properObject = ProperClass(name: "ProperObject")
        
        // Track them
        MemoryLeakDetector.track(leakyObject, name: "LeakyObject")
        MemoryLeakDetector.track(properObject, name: "ProperObject")
        
        // Create retain cycle in leaky object
        leakyObject.createRetainCycle()
        
        // Proper object doesn't create retain cycle
        properObject.cleanup()
        
        // Check for leaks
        MemoryLeakDetector.checkForLeaks()
    }
}

class LeakyClass {
    let name: String
    private var retainCycle: (() -> Void)?
    
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    
    deinit {
        print("\(name) deallocated")
    }
    
    func createRetainCycle() {
        retainCycle = {
            print("Hello from \(self.name)")
        }
    }
}

class ProperClass {
    let name: String
    private var weakClosure: (() -> Void)?
    
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    
    deinit {
        print("\(name) deallocated")
    }
    
    func createWeakClosure() {
        weakClosure = { [weak self] in
            guard let self = self else { return }
            print("Hello from \(self.name)")
        }
    }
    
    func cleanup() {
        weakClosure = nil
    }
}

7. Performance Monitoring and Best Practices

// MARK: - Memory Performance Monitor
class MemoryPerformanceMonitor {
    static let shared = MemoryPerformanceMonitor()
    
    private var memorySnapshots: [String: MemorySnapshot] = [:]
    
    func takeSnapshot(name: String) {
        let snapshot = MemorySnapshot()
        memorySnapshots[name] = snapshot
        print("Memory snapshot taken: \(name)")
    }
    
    func compareSnapshots(before: String, after: String) {
        guard let beforeSnapshot = memorySnapshots[before],
              let afterSnapshot = memorySnapshots[after] else {
            print("Snapshots not found")
            return
        }
        
        let difference = afterSnapshot.memoryUsage - beforeSnapshot.memoryUsage
        print("Memory difference: \(difference) bytes")
        
        if difference > 1024 * 1024 { // 1MB
            print("⚠️ Significant memory increase detected")
        }
    }
}

struct MemorySnapshot {
    let memoryUsage: Int
    
    init() {
        var info = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
        
        let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                task_info(mach_task_self_,
                         task_flavor_t(MACH_TASK_BASIC_INFO),
                         $0,
                         &count)
            }
        }
        
        if kerr == KERN_SUCCESS {
            memoryUsage = Int(info.resident_size)
        } else {
            memoryUsage = 0
        }
    }
}

// MARK: - Best Practices Implementation
class MemoryBestPractices {
    // MARK: - Use weak references for delegates
    weak var delegate: SomeDelegate?
    
    // MARK: - Use capture lists in closures
    func performAsyncWork(completion: @escaping () -> Void) {
        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            
            // Do work
            self.processData()
            
            DispatchQueue.main.async {
                completion()
            }
        }
    }
    
    private func processData() {
        // Process data
    }
    
    // MARK: - Clean up resources
    func cleanup() {
        delegate = nil
        // Clean up other resources
    }
}

protocol SomeDelegate: AnyObject {
    func didComplete()
}

Summary

Effective memory management in iOS requires:

  1. Understanding ARC: Know how reference counting works
  2. Avoiding Retain Cycles: Use weak and unowned references appropriately
  3. Closure Capture Lists: Always consider memory implications in closures
  4. SwiftUI Memory Management: Use @StateObject and @ObservedObject correctly
  5. Leak Detection: Implement monitoring and detection tools
  6. Best Practices: Follow established patterns for memory safety

By mastering these concepts, you can build iOS applications that are both performant and memory-efficient.