Understanding Automatic Reference Counting (ARC)
Automatic Reference Counting (ARC) manages memory automatically in Swift by tracking how many references point to each object. When the reference count reaches zero, ARC deallocates the object.
However, ARC can't handle reference cycles automatically. This is where understanding different reference types becomes crucial for preventing memory leaks and crashes.
1. Strong References (Default)
Strong references are the default in Swift. They increase the reference count and prevent the object from being deallocated while the reference exists.
Basic Strong Reference Example
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
// Strong reference example
var person1: Person? = Person(name: "John") // Reference count: 1
var person2: Person? = person1 // Reference count: 2
person1 = nil // Reference count: 1
person2 = nil // Reference count: 0, object is deallocated
When to Use Strong References
- Parent-to-child relationships
- Owner-to-owned object relationships
- When you need to ensure the object stays alive
- Default choice for most properties
2. Weak References
Weak references don't increase the reference count and automatically become nil when the referenced object is deallocated. They must always be optional variables.
Weak Reference Example
class Apartment {
let number: Int
weak var tenant: Person? // weak reference
init(number: Int) {
self.number = number
}
deinit {
print("Apartment \(number) is being deinitialized")
}
}
class Person {
let name: String
var apartment: Apartment? // strong reference
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
// Usage
var john: Person? = Person(name: "John")
var apartment: Apartment? = Apartment(number: 73)
john?.apartment = apartment // Person strongly references Apartment
apartment?.tenant = john // Apartment weakly references Person
john = nil // Person is deallocated
print(apartment?.tenant) // nil - weak reference automatically set to nil
Common Weak Reference Patterns
// Delegate pattern
protocol NetworkManagerDelegate: AnyObject {
func didReceiveData(_ data: Data)
func didFailWithError(_ error: Error)
}
class NetworkManager {
weak var delegate: NetworkManagerDelegate? // Prevent retain cycle
func fetchData() {
// Network request logic
delegate?.didReceiveData(responseData)
}
}
// Closure capture lists
class ViewController: UIViewController {
var networkManager = NetworkManager()
override func viewDidLoad() {
super.viewDidLoad()
// ❌ Strong reference cycle
networkManager.completion = { data in
self.updateUI(with: data) // self is captured strongly
}
// ✅ Weak reference prevents cycle
networkManager.completion = { [weak self] data in
self?.updateUI(with: data) // self is captured weakly
}
}
private func updateUI(with data: Data) {
// Update UI
}
}
When to Use Weak References
- Delegate properties
- Child-to-parent references
- Breaking retain cycles in closures
- Observer patterns
- When the referenced object might be deallocated
3. Unowned References
Unowned references don't increase the reference count but assume the referenced object will always be available. They're not optional and will crash if accessed after the object is deallocated.
Unowned Reference Example
class Customer {
let name: String
var creditCard: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // unowned reference
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
// Usage
var john: Customer? = Customer(name: "John Appleseed")
john?.creditCard = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil // Both Customer and CreditCard are deallocated
Unowned vs Weak Decision Tree
// Use weak when the reference might become nil
class Parent {
var children: [Child] = []
}
class Child {
weak var parent: Parent? // Child might outlive parent
}
// Use unowned when the reference should never become nil
class View {
unowned let controller: ViewController // View should never outlive controller
init(controller: ViewController) {
self.controller = controller
}
}
// Real-world example: Network request with callback
class APIClient {
func fetchUser(completion: @escaping (User?) -> Void) {
// Network request
}
}
class UserViewController: UIViewController {
let apiClient = APIClient()
func loadUser() {
// Use weak because ViewController might be deallocated before request completes
apiClient.fetchUser { [weak self] user in
self?.updateUI(with: user)
}
}
func loadCriticalData() {
// Use unowned only if you're certain ViewController will exist
// when the closure executes (generally not recommended for async operations)
apiClient.fetchUser { [unowned self] user in
self.updateUI(with: user) // Will crash if self is deallocated
}
}
}
When to Use Unowned References
- When the referenced object will always outlive the current object
- One-to-one relationships where objects have the same lifetime
- When you want to avoid optional unwrapping
- Performance-critical code (slight performance advantage over weak)
Memory Leaks in Real Applications
Common Retain Cycle Scenarios
// 1. Timer retain cycles
class TimerViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// ❌ Timer strongly retains target
timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(timerFired),
userInfo: nil,
repeats: true)
}
// ✅ Use weak reference or invalidate timer
deinit {
timer?.invalidate() // Essential to break the cycle
}
@objc func timerFired() {
// Update UI
}
}
// 2. Notification center retain cycles
class NotificationViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ❌ Can create retain cycle with certain notification patterns
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .init("CustomNotification"),
object: nil
)
}
// ✅ Always remove observers
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func handleNotification() {
// Handle notification
}
}
// 3. URLSession retain cycles
class NetworkViewController: UIViewController {
func fetchData() {
// ❌ Potential retain cycle
URLSession.shared.dataTask(with: url) { data, response, error in
self.processData(data) // Strong capture of self
}.resume()
// ✅ Use weak capture
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.processData(data)
}.resume()
}
}
Advanced Memory Management Patterns
// Custom weak reference wrapper
class WeakWrapper {
weak var value: T?
init(_ value: T) {
self.value = value
}
}
// Weak array for observers
class EventEmitter {
private var observers: [WeakWrapper] = []
func addObserver(_ observer: EventObserver) {
observers.append(WeakWrapper(observer))
}
func removeObserver(_ observer: EventObserver) {
observers.removeAll { $0.value === observer }
}
func emit() {
// Clean up nil references
observers = observers.filter { $0.value != nil }
// Notify active observers
observers.forEach { $0.value?.handleEvent() }
}
}
// Lazy initialization with unowned
class ExpensiveResource {
unowned let owner: ResourceOwner
init(owner: ResourceOwner) {
self.owner = owner
// Expensive setup
}
}
class ResourceOwner {
lazy var resource: ExpensiveResource = ExpensiveResource(owner: self)
}
Debugging Memory Issues
Using Instruments
- Leaks Instrument: Detects retain cycles and memory leaks
- Allocations Instrument: Tracks memory usage patterns
- VM Tracker: Monitors virtual memory usage
Debug Techniques
// Add deinit methods to verify deallocation
class MyViewController: UIViewController {
deinit {
print("MyViewController deallocated")
// This should print when the VC is dismissed
// If it doesn't, you have a retain cycle
}
}
// Use memory debugging tools
// In Xcode: Product → Profile → Leaks
// Runtime debugging: Environment Variables → MallocStackLogging = 1
// Weak reference debugging
class DebuggableWeakRef {
weak var value: T? {
didSet {
if value == nil {
print("Weak reference became nil")
}
}
}
init(_ value: T) {
self.value = value
}
}
Best Practices Summary
Reference Type | Use When | Characteristics |
---|---|---|
Strong | Parent-to-child, ownership | Default, increases retain count |
Weak | Delegates, child-to-parent | Optional, becomes nil automatically |
Unowned | Equal lifetimes, performance critical | Non-optional, crashes if accessed after dealloc |
Quick Decision Guide
- Default to strong unless you have a specific reason
- Use weak for delegates and back-references
- Use weak in closures that might outlive the object
- Use unowned sparingly and only when you're certain of lifetimes
- Always test memory management with Instruments
Common Mistakes to Avoid
- Using unowned when the object might be deallocated (causes crashes)
- Forgetting to use weak in delegate properties
- Creating retain cycles with closures
- Not invalidating timers in deinit
- Assuming ARC handles all memory management automatically
Production Experience
These patterns and debugging techniques are documented in my iOS-Notes repository, including examples of memory leaks I've encountered and fixed in production applications.