Detectarea și Eliminarea Retain Cycles în Swift: Gestiunea Memoriei cu ARC, [weak self] și Instrumentele Xcode în Swift (iOS & macOS)

Ghid complet de producție pentru prevenirea scurgerilor de memorie în Swift: mecanismul ARC, diferențierea critică între weak și unowned, capture lists în închideri și depanarea vizuală prin Xcode Memory Graph Debugger și Instruments Leaks.

Sintaxă Swift
[weak self] in guard let self else { return } | Instruments Leaks

În dezvoltarea aplicațiilor iOS și macOS, scurgerile de memorie (Memory Leaks) cauzate de cicluri de referință puternică (Retain Cycles) reprezintă cauza principală pentru degradarea treptată a performanței, creșterea amprentei RAM și oprirea forțată a aplicației de către sistemul de operare prin jetonul EXC_RESOURCE (WAKEUPS / MEMORY).

Deoarece Swift utilizează Automatic Reference Counting (ARC), o zonă de memorie pe heap este eliberată exclusiv atunci când contorul său de referințe puternice ajunge la zero (reference count == 0). Dacă două obiecte dețin referințe puternice reciproce, contorul niciunuia nu va atinge niciodată zero, rămânând blocate pe heap la infinit.

Anatomia unui Retain Cycle Clasiv: Closure către Clasă
// SCENARIU VULNERABIL:
class DataService {
    var onDataLoaded: ((String) -> Void)?
    var cachedPayload: String = "Date inițiale"

    func setupListener() {
        // PERICOL: Closure-ul capturează 'self' în mod puternic (strong reference)!
        // DataService deține closure-ul prin proprietatea 'onDataLoaded',
        // iar closure-ul deține instanța DataService prin 'self'. REZULTAT: RETAIN CYCLE!
        self.onDataLoaded = { date in
            print("Date primite: \(date), cache existent: \(self.cachedPayload)")
        }
    }
}

// SOLUȚIE CORECTĂ: Folosirea unei Capture List cu [weak self]:
class SafeDataService {
    var onDataLoaded: ((String) -> Void)?
    var cachedPayload: String = "Date inițiale"

    func setupListener() {
        // [weak self] transformă referința într-un Optional<SafeDataService> zeroabil:
        self.onDataLoaded = { [weak self] date in
            // Unwrapping idiomatic Swift 5.7+ (guard let self):
            guard let self else {
                print("Instanța a fost dealocată; abandonăm execuția.")
                return
            }
            print("Date primite: \(date), cache existent: \(self.cachedPayload)")
        }
    }

    deinit {
        print("SafeDataService a fost eliberat complet din memorie!")
    }
}

weak vs. unowned: Când și Ce Alegem?

Criteriu weak unowned
Tip de Date Întotdeauna opțional (T?). Tip direct non-opțional (T).
Comportament la Dealocare ARC setează automat valoarea pe nil prin tabele laterale (Side Tables). Pointer-ul rămâne suspendat; accesarea provoacă oprire forțată (Runtime Trap).
Recomandare de Uz Opțiunea implicită și sigură (delegates, closures asincrone de rețea). Relații în care obiectul referit este garantat să aibă o durată de viață identică sau mai lungă.

Depanarea Retain Cycles cu Uneltele Oficiale Xcode

  1. Xcode Memory Graph Debugger:

    În timpul rulării aplicației în simulator sau dispozitiv fizic, apăsați pe pictograma cu trei cercuri conectate (Debug Memory Graph) din bara de unelte inferioară. Xcode suspendă procesul și construiește graful complet de alocări. În panoul din stânga, orice retain cycle detectat este evidențiat cu un semn de exclamare violet (Runtime Warning: Memory Leak). Făcând clic pe nod, vizualizați lanțul exact de pointeri puternici care blochează eliberarea.

  2. Xcode Instruments (Profilul Leaks & Allocations):

    Lansați Xcode -> Open Developer Tool -> Instruments -> Leaks. Înregistrați sesiunea de utilizare navigând repetat într-un ecran și ieșind din el. Dacă bara de stare indică stegulețe roșii (Red Flags), accesați Call Tree și bifați Invert Call Tree și Hide System Libraries pentru a izola linia exactă de cod unde s-a alocat instanța pierdută.

Test Unitar Automatizat pentru Detectarea Retain Cycles (XCTest)
import XCTest
@testable import MyApp

final class MemoryLeakTests: XCTestCase {
    func test_viewModel_doesNotLeakOnDismiss() {
        var sut: UserViewModel? = UserViewModel()
        
        // Înregistrăm o verificare automată la finalul testului:
        addTeardownBlock { [weak sut] in
            XCTAssertNil(sut, "CRITIC: UserViewModel deține un retain cycle și nu a fost eliberat din memorie!")
        }
        
        // Simulăm fluxul de lucru:
        sut?.incarcaDate()
        
        // La finalul scope-ului, eliberăm instanța:
        sut = nil
    }
}