iOS Advanced Memory Management: ARC, Weak References, and Memory Leaks
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:
- Understanding ARC: Know how reference counting works
- Avoiding Retain Cycles: Use weak and unowned references appropriately
- Closure Capture Lists: Always consider memory implications in closures
- SwiftUI Memory Management: Use @StateObject and @ObservedObject correctly
- Leak Detection: Implement monitoring and detection tools
- Best Practices: Follow established patterns for memory safety
By mastering these concepts, you can build iOS applications that are both performant and memory-efficient.