bookworm-smart-assistant/skills/swift-expert/references/memory-performance.md

8.4 KiB

Memory & Performance

Automatic Reference Counting (ARC)

// Strong references (default)
class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    weak var tenant: Person?  // Weak to break retain cycle

    init(unit: String) {
        self.unit = unit
    }

    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

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

john?.apartment = unit4A
unit4A?.tenant = john

// Setting to nil will properly deallocate both
john = nil
unit4A = nil

Weak and Unowned References

// Weak - optional reference that doesn't keep object alive
class ViewController: UIViewController {
    weak var delegate: ViewControllerDelegate?

    func performAction() {
        delegate?.didPerformAction()
    }
}

// Unowned - non-optional reference, assumes target outlives owner
class Customer {
    let name: String
    var card: CreditCard?

    init(name: String) {
        self.name = name
    }
}

class CreditCard {
    let number: String
    unowned let customer: Customer  // Customer always outlives card

    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

// Unowned optional (Swift 5+)
class Department {
    var courses: [Course] = []
}

class Course {
    unowned var department: Department
    unowned var nextCourse: Course?

    init(department: Department) {
        self.department = department
    }
}

Capture Lists in Closures

class DataManager {
    var data: [String] = []

    func loadData() {
        // Strong reference cycle - DataManager won't be deallocated
        NetworkManager.fetch { response in
            self.data = response  // self is captured strongly
        }

        // Weak self - breaks cycle
        NetworkManager.fetch { [weak self] response in
            guard let self = self else { return }
            self.data = response
        }

        // Unowned self - when self definitely outlives closure
        NetworkManager.fetch { [unowned self] response in
            self.data = response  // Crashes if self is deallocated
        }

        // Capturing specific values
        let identifier = UUID()
        NetworkManager.fetch { [identifier] response in
            print("Request \(identifier) completed")
        }
    }
}

Value Semantics

// Structs provide automatic copy-on-write for collections
struct User {
    var name: String
    var friends: [String]  // Copy-on-write
}

var user1 = User(name: "Alice", friends: ["Bob"])
var user2 = user1  // Shallow copy
user2.friends.append("Charlie")  // Now triggers deep copy

print(user1.friends)  // ["Bob"]
print(user2.friends)  // ["Bob", "Charlie"]

// Custom copy-on-write
final class Storage<T> {
    var value: T
    init(_ value: T) { self.value = value }
}

struct MyArray<Element> {
    private var storage: Storage<[Element]>

    init(_ elements: [Element] = []) {
        storage = Storage(elements)
    }

    var value: [Element] {
        get { storage.value }
        set {
            if !isKnownUniquelyReferenced(&storage) {
                storage = Storage(newValue)
            } else {
                storage.value = newValue
            }
        }
    }

    mutating func append(_ element: Element) {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(storage.value)
        }
        storage.value.append(element)
    }
}

Performance Optimization

// Use lazy properties for expensive computations
class Report {
    let data: [DataPoint]

    lazy var summary: String = {
        // Expensive computation only when accessed
        data.map { $0.description }.joined(separator: "\n")
    }()

    init(data: [DataPoint]) {
        self.data = data
    }
}

// Avoid repeated type casting
// Bad
for item in items {
    if let user = item as? User {
        processUser(user)
    }
}

// Good
let users = items.compactMap { $0 as? User }
for user in users {
    processUser(user)
}

// Use contiguous storage
// Slower - pointer indirection for each element
let arrayOfClasses: [MyClass] = [MyClass(), MyClass()]

// Faster - contiguous memory
let arrayOfStructs: [MyStruct] = [MyStruct(), MyStruct()]

// Avoid string concatenation in loops
// Bad
var result = ""
for item in items {
    result += item.description  // Allocates new string each time
}

// Good
let result = items.map { $0.description }.joined()

// Or
var result = ""
result.reserveCapacity(estimatedSize)
for item in items {
    result.append(item.description)
}

Collection Performance

// Choose the right collection type
// Array - ordered, random access O(1), append O(1) amortized
let ordered: [Int] = [1, 2, 3]

// Set - unique elements, contains O(1), no order
let unique: Set<Int> = [1, 2, 3]

// Dictionary - key-value pairs, lookup O(1)
let mapping: [String: Int] = ["a": 1, "b": 2]

// Use ContiguousArray for performance-critical code
let contiguous = ContiguousArray<MyStruct>(repeating: MyStruct(), count: 1000)

// Reserve capacity for known sizes
var numbers: [Int] = []
numbers.reserveCapacity(1000)
for i in 0..<1000 {
    numbers.append(i)
}

// Use enumerated() instead of indices
// Bad
for i in 0..<array.count {
    process(index: i, value: array[i])
}

// Good
for (index, value) in array.enumerated() {
    process(index: index, value: value)
}

Memory Profiling with Instruments

// Add markers for profiling
import os.signpost

let log = OSLog(subsystem: "com.example.app", category: "Performance")

func processData() {
    os_signpost(.begin, log: log, name: "Data Processing")
    defer { os_signpost(.end, log: log, name: "Data Processing") }

    // Processing code
}

// Autoreleasepool for memory-intensive loops
func processLargeDataset() {
    for batch in dataBatches {
        autoreleasepool {
            // Process batch
            // Memory released at end of each iteration
        }
    }
}

// Check for memory leaks
#if DEBUG
extension NSObject {
    static func trackAllocations() {
        let count = performSelector(
            Selector(("instancesRespond:"))
        )
        print("\(self): \(count) instances")
    }
}
#endif

Optimization Levels

// Whole Module Optimization in Package.swift
let package = Package(
    name: "MyApp",
    products: [
        .executable(name: "MyApp", targets: ["MyApp"])
    ],
    targets: [
        .target(
            name: "MyApp",
            swiftSettings: [
                .unsafeFlags(["-O"], .when(configuration: .release))
            ]
        )
    ]
)

// Inline optimization
@inline(__always)
func criticalPath() {
    // Always inlined
}

@inline(never)
func debugHelper() {
    // Never inlined, good for debugging
}

// Optimization attributes
@_specialize(where T == Int)
@_specialize(where T == String)
func process<T>(_ value: T) {
    // Specialized versions generated
}

Memory Warnings

class ImageCache {
    private var cache: [String: UIImage] = [:]

    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(clearCache),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }

    @objc private func clearCache() {
        cache.removeAll()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

Best Practices

  • Use value types (structs) by default
  • Use weak references for delegates
  • Use unowned when lifetime is guaranteed
  • Always use capture lists in closures that reference self
  • Profile before optimizing (use Instruments)
  • Reserve collection capacity when size is known
  • Use lazy properties for expensive computations
  • Implement copy-on-write for custom types with reference storage
  • Handle memory warnings in iOS apps
  • Use autoreleasepool for memory-intensive loops
  • Choose appropriate collection types
  • Avoid premature optimization - measure first