Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/memory-management.md
1# Memory Management23Use this when:45- A task or async sequence is keeping objects alive longer than expected.6- You suspect a retain cycle between a task and its owner.7- You need to verify deallocation behavior or use `isolated deinit`.89Skip this file if:1011- You mainly need to protect mutable state from races. Use `actors.md`.12- You are debugging slow async code. Use `performance.md`.1314Jump to:1516- Core Concepts (Task Capture)17- Retain Cycles18- One-Way Retention19- Async Sequences and Retention20- Isolated Deinit (Swift 6.2+)21- Detection and Testing22- Common Patterns2324## Core Concepts2526### Tasks capture like closures2728Tasks capture variables and references just like regular closures. Swift doesn't automatically prevent retain cycles in concurrent code.2930```swift31Task {32self.doWork() // ⚠️ Strong capture of self33}34```3536### Why concurrency hides memory issues3738- Tasks may live longer than expected39- Async operations delay execution40- Harder to track when memory should be released41- Long-running tasks can hold references indefinitely4243> **Course Deep Dive**: This topic is covered in detail in [Lesson 8.1: Overview of memory management in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)4445## Retain Cycles4647### What is a retain cycle?4849Two or more objects hold strong references to each other, preventing deallocation.5051```swift52class A {53var b: B?54}5556class B {57var a: A?58}5960let a = A()61let b = B()62a.b = b63b.a = a // Retain cycle - neither can be deallocated64```6566### Retain cycles with Tasks6768When task captures `self` strongly and `self` owns the task:6970```swift71@MainActor72final class ImageLoader {73var task: Task<Void, Never>?7475func startPolling() {76task = Task {77while true {78self.pollImages() // ⚠️ Strong capture79try? await Task.sleep(for: .seconds(1))80}81}82}83}8485var loader: ImageLoader? = .init()86loader?.startPolling()87loader = nil // ⚠️ Loader never deallocated - retain cycle!88```8990**Problem**: Task holds `self`, `self` holds task → neither released.9192## Breaking Retain Cycles9394### Use weak self9596```swift97func startPolling() {98task = Task { [weak self] in99while let self = self {100self.pollImages()101try? await Task.sleep(for: .seconds(1))102}103}104}105106var loader: ImageLoader? = .init()107loader?.startPolling()108loader = nil // ✅ Loader deallocated, task stops109```110111### Pattern for long-running tasks112113```swift114task = Task { [weak self] in115while let self = self {116await self.doWork()117try? await Task.sleep(for: interval)118}119}120```121122> **Course Deep Dive**: This topic is covered in detail in [Lesson 8.2: Preventing retain cycles when using Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)123124Loop exits when `self` becomes `nil`.125126## One-Way Retention127128Task retains `self`, but `self` doesn't retain task. Object stays alive until task completes.129130```swift131@MainActor132final class ViewModel {133func fetchData() {134Task {135await performRequest()136updateUI() // ⚠️ Strong capture137}138}139}140141var viewModel: ViewModel? = .init()142viewModel?.fetchData()143viewModel = nil // ViewModel stays alive until task completes144```145146**Execution order**:1471. Task starts1482. `viewModel = nil` (but object not deallocated)1493. Task completes1504. ViewModel finally deallocated151152### When one-way retention is acceptable153154Short-lived tasks that complete quickly:155156```swift157func saveData() {158Task {159await database.save(self.data) // OK - completes quickly160}161}162```163164### When to use weak self165166Long-running or indefinite tasks:167168```swift169func startMonitoring() {170Task { [weak self] in171for await event in eventStream {172self?.handle(event)173}174}175}176```177178## Async Sequences and Retention179180### Problem: Infinite sequences181182```swift183@MainActor184final class AppLifecycleViewModel {185private(set) var isActive = false186private var task: Task<Void, Never>?187188func startObserving() {189task = Task {190for await _ in NotificationCenter.default.notifications(191named: .didBecomeActive192) {193isActive = true // ⚠️ Strong capture, never ends194}195}196}197}198199var viewModel: AppLifecycleViewModel? = .init()200viewModel?.startObserving()201viewModel = nil // ⚠️ Never deallocated - sequence continues202```203204**Problem**: Async sequence never finishes, task holds `self` indefinitely.205206### Solution 1: Manual cancellation207208```swift209func startObserving() {210task = Task {211for await _ in NotificationCenter.default.notifications(212named: .didBecomeActive213) {214isActive = true215}216}217}218219func stopObserving() {220task?.cancel()221}222223// Usage224viewModel?.startObserving()225viewModel?.stopObserving() // Must call before release226viewModel = nil227```228229### Solution 2: Weak self with guard230231```swift232func startObserving() {233task = Task { [weak self] in234for await _ in NotificationCenter.default.notifications(235named: .didBecomeActive236) {237guard let self = self else { return }238self.isActive = true239}240}241}242```243244Task exits when `self` deallocates.245246## Isolated deinit (Swift 6.2+)247248Clean up actor-isolated state in deinit:249250```swift251@MainActor252final class ViewModel {253private var task: Task<Void, Never>?254255isolated deinit {256task?.cancel()257}258}259```260261**Limitation**: Won't break retain cycles (deinit never called if cycle exists).262263**Use for**: Cleanup when object is being deallocated normally.264265## Common Patterns266267### Short-lived task (strong capture OK)268269```swift270func saveData() {271Task {272await database.save(self.data)273self.updateUI()274}275}276```277278**When safe**: Task completes quickly, acceptable for object to live until done.279280### Long-running task (weak self required)281282```swift283func startPolling() {284task = Task { [weak self] in285while let self = self {286await self.fetchUpdates()287try? await Task.sleep(for: .seconds(5))288}289}290}291```292293### Async sequence monitoring (weak self + guard)294295```swift296func startMonitoring() {297task = Task { [weak self] in298for await event in eventStream {299guard let self = self else { return }300self.handle(event)301}302}303}304```305306### Cancellable work with cleanup307308```swift309func startWork() {310task = Task { [weak self] in311defer { self?.cleanup() }312313while let self = self {314await self.doWork()315try? await Task.sleep(for: .seconds(1))316}317}318}319```320321## Detection Strategies322323### Add deinit logging324325```swift326deinit {327print("✅ \(type(of: self)) deallocated")328}329```330331If deinit never prints → likely retain cycle.332333### Memory graph debugger3343351. Run app in Xcode3362. Debug → Debug Memory Graph3373. Look for cycles in object graph338339### Instruments340341Use Leaks instrument to detect retain cycles at runtime.342343## Decision Tree344345```346Task captures self?347├─ Task completes quickly?348│ └─ Strong capture OK349│350├─ Long-running or infinite?351│ ├─ Can use weak self? → Use [weak self]352│ ├─ Need manual control? → Store task, cancel explicitly353│ └─ Async sequence? → [weak self] + guard354│355└─ Self owns task?356├─ Yes → High risk of retain cycle357└─ No → Lower risk, but check lifetime358```359360## Best Practices3613621. **Default to weak self** for long-running tasks3632. **Use guard let self** in async sequences3643. **Cancel tasks explicitly** when possible3654. **Add deinit logging** during development3665. **Test object deallocation** in unit tests3676. **Use Memory Graph** to verify no cycles3687. **Document lifetime expectations** in comments3698. **Prefer cancellation** over weak self when possible3709. **Avoid nested strong captures** in task closures37110. **Use isolated deinit** for cleanup (Swift 6.2+)372373## Testing for Leaks374375### Unit test pattern376377```swift378func testViewModelDeallocates() async {379var viewModel: ViewModel? = ViewModel()380weak var weakViewModel = viewModel381382viewModel?.startWork()383viewModel = nil384385// Give tasks time to complete386try? await Task.sleep(for: .milliseconds(100))387388XCTAssertNil(weakViewModel, "ViewModel should be deallocated")389}390```391392### SwiftUI view test393394```swift395func testViewDeallocates() {396var view: MyView? = MyView()397weak var weakView = view398399view = nil400401XCTAssertNil(weakView)402}403```404405## Common Mistakes406407### ❌ Forgetting weak self in loops408409```swift410Task {411while true {412self.poll() // Retain cycle413try? await Task.sleep(for: .seconds(1))414}415}416```417418### ❌ Strong capture in async sequences419420```swift421Task {422for await item in stream {423self.process(item) // May never release424}425}426```427428### ❌ Not canceling stored tasks429430```swift431class Manager {432var task: Task<Void, Never>?433434func start() {435task = Task {436await self.work() // Retain cycle437}438}439440// Missing: deinit { task?.cancel() }441}442```443444### ❌ Assuming deinit breaks cycles445446```swift447deinit {448task?.cancel() // Never called if retain cycle exists449}450```451452## Examples by Use Case453454### Polling service455456```swift457final class PollingService {458private var task: Task<Void, Never>?459460func start() {461task = Task { [weak self] in462while let self = self {463await self.poll()464try? await Task.sleep(for: .seconds(5))465}466}467}468469func stop() {470task?.cancel()471}472}473```474475### Notification observer476477```swift478@MainActor479final class NotificationObserver {480private var task: Task<Void, Never>?481482func startObserving() {483task = Task { [weak self] in484for await notification in NotificationCenter.default.notifications(485named: .someNotification486) {487guard let self = self else { return }488self.handle(notification)489}490}491}492493isolated deinit {494task?.cancel()495}496}497```498499### Download manager500501```swift502final class DownloadManager {503private var tasks: [URL: Task<Data, Error>] = [:]504505func download(_ url: URL) async throws -> Data {506let task = Task { [weak self] in507defer { self?.tasks.removeValue(forKey: url) }508return try await URLSession.shared.data(from: url).0509}510511tasks[url] = task512return try await task.value513}514515func cancelAll() {516tasks.values.forEach { $0.cancel() }517tasks.removeAll()518}519}520```521522### Timer523524```swift525actor Timer {526private var task: Task<Void, Never>?527528func start(interval: Duration, action: @Sendable () async -> Void) {529task = Task {530while !Task.isCancelled {531await action()532try? await Task.sleep(for: interval)533}534}535}536537func stop() {538task?.cancel()539}540}541```542543## Common Mistakes Agents Make544545- **Forgetting `[weak self]` in stored tasks**: When `self` owns the task and the task captures `self`, a retain cycle prevents deallocation.546- **Strong capture in infinite `AsyncSequence` loops**: `for await` over an infinite sequence with a strong `self` capture keeps the object alive forever.547- **Not cancelling stored tasks on cleanup**: If the task outlives its owner, it retains captured objects indefinitely.548- **Assuming `isolated deinit` breaks retain cycles**: `isolated deinit` runs cleanup on the correct actor, but if a cycle prevents `deinit` from being called at all, the cleanup never executes.549- **Using `try?` in loops with `Task.sleep`**: `try?` can swallow `CancellationError`, causing the loop to continue running after cancellation. Always check `Task.isCancelled` explicitly.550551## Debugging Checklist552553When object won't deallocate:554555- [ ] Check for strong self captures in tasks556- [ ] Verify tasks are canceled or complete557- [ ] Look for infinite loops or sequences558- [ ] Check if self owns the task559- [ ] Use Memory Graph to find cycles560- [ ] Add deinit logging to verify561- [ ] Test with weak references562- [ ] Review async sequence usage563- [ ] Check nested task captures564- [ ] Verify cleanup in deinit565566## Further Learning567568For migration strategies, real-world examples, and advanced memory patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).569570