Memory Management
Swift uses Automatic Reference Counting (ARC) to manage memory for class instances, deallocating objects when no longer referenced. This document covers ARC, reference types, retain cycles, and memory optimization.
ARC Basics
ARC tracks strong references to class instances, deallocating them when the reference count reaches zero. Value types (structs, enums) are copied, not referenced.
Example: ARC in Action:
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
var person: Person? = Person(name: "Alice")
person = nil // "Alice deallocated"
Reference Types
- Strong References: Default, increment reference count.
- Weak References: Do not increment count, become
nil
when deallocated. - Unowned References: Assume non-nil, do not increment count, crash if deallocated.
Example: Strong Reference:
class Apartment {
var tenant: Person?
}
let apt = Apartment()
apt.tenant = Person(name: "Bob")
Example: Weak Reference:
class Apartment {
weak var tenant: Person?
}
var person: Person? = Person(name: "Charlie")
let apt = Apartment()
apt.tenant = person
person = nil // tenant becomes nil, "Charlie deallocated"
Example: Unowned Reference:
class CreditCard {
unowned let owner: Person
init(owner: Person) { self.owner = owner }
}
let owner = Person(name: "Dave")
let card = CreditCard(owner: owner)
// owner = nil // Would crash if accessed
Retain Cycles
Retain cycles occur when objects strongly reference each other, preventing deallocation.
Example: Retain Cycle:
class Person {
var apartment: Apartment?
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
class Apartment {
var tenant: Person?
deinit { print("Apartment deallocated") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment()
alice?.apartment = apt
apt?.tenant = alice // Retain cycle
alice = nil // No deallocation
apt = nil // No deallocation
Fixing with Weak Reference:
class Apartment {
weak var tenant: Person?
deinit { print("Apartment deallocated") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment()
alice?.apartment = apt
apt?.tenant = alice
alice = nil // "Alice deallocated"
apt = nil // "Apartment deallocated"
Closure Capture Lists
Closures can cause retain cycles by capturing self
strongly. Use capture lists ([weak self]
, [unowned self]
) to manage references.
Example: Closure Retain Cycle:
class ViewController {
var name = "View"
var handler: (() -> Void)?
func setup() {
handler = {
print(self.name) // Strong capture
}
}
deinit { print("ViewController deallocated") }
}
var vc: ViewController? = ViewController()
vc?.setup()
vc = nil // Not deallocated due to retain cycle
Fixing with Weak Capture:
func setup() {
handler = { [weak self] in
print(self?.name ?? "Nil")
}
}
var vc: ViewController? = ViewController()
vc?.setup()
vc = nil // "ViewController deallocated"
Lazy Properties and ARC
Lazy properties can create retain cycles if they capture self
.
Example:
class DataManager {
lazy var data: [Int] = {
return [self.count] // Strong capture
}()
let count = 42
deinit { print("DataManager deallocated") }
}
var manager: DataManager? = DataManager()
_ = manager?.data
manager = nil // Not deallocated
Fix with Unowned:
lazy var data: [Int] = { [unowned self] in
return [self.count]
}()
Memory Optimization
- Copy-on-Write: Value types like
Array
use copy-on-write to minimize copying. - Inout Parameters: Pass large structs
inout
to avoid copying. - Weak/Unowned: Use appropriately to reduce memory usage.
Example: Copy-on-Write:
var array1 = [1, 2, 3]
var array2 = array1 // Shares storage
array2.append(4) // Copies storage
print(array1) // [1, 2, 3]
print(array2) // [1, 2, 3, 4]
Debugging Memory Issues
Use Xcode’s Instruments (Memory Graph Debugger, Leaks) to identify retain cycles and leaks.
Example Workflow:
- Run app in Xcode.
- Use Debug Memory Graph to visualize references.
- Check for unexpected strong references.
Best Practices
- Weak for Optionals: Use
weak
for references that can becomenil
. - Unowned for Non-Nil: Use
unowned
when references are guaranteed. - Capture Lists: Always consider closure captures.
- Deinit Testing: Ensure
deinit
is called as expected. - Minimize Strong References: Reduce object interdependencies.
- Profile Regularly: Use Instruments for large projects.
Troubleshooting
- Retain Cycles: Check for strong references in closures or properties.
- Crashes on Unowned: Verify
unowned
references are valid. - Memory Leaks: Use Instruments to trace unreleased objects.
- Unexpected Deallocation: Ensure weak references aren’t overused.
- Performance: Optimize copy-on-write for large collections.
Example: Comprehensive Memory Management
class Library {
var books: [Book] = []
weak var librarian: Person?
deinit { print("Library deallocated") }
}
class Book {
unowned let library: Library
let title: String
init(library: Library, title: String) {
self.library = library
self.title = title
}
deinit { print("Book \(title) deallocated") }
}
class Person {
let name: String
var handler: (() -> Void)?
init(name: String) {
self.name = name
setup()
}
func setup() {
handler = { [weak self] in
print("Handler for \(self?.name ?? "nil")")
}
}
deinit { print("\(name) deallocated") }
}
var library: Library? = Library()
var person: Person? = Person(name: "Eve")
library?.librarian = person
library?.books.append(Book(library: library!, title: "Swift Guide"))
person?.handler?() // "Handler for Eve"
person = nil // "Eve deallocated"
library = nil // "Library deallocated", "Book Swift Guide deallocated"