Sobes.tech
Middle

Cosa sono i cicli di ritenzione (retain cycles) in iOS?

sobes.tech AI

Risposta dell'AI

I cicli di ritenzione si verificano quando due o più oggetti hanno riferimenti forti tra loro, creando un "cerchio chiuso". Di conseguenza, il contatore di riferimenti di ogni oggetto non raggiunge mai zero e non può essere liberato da ARC (Automatic Reference Counting). Ciò porta a perdite di memoria, poiché gli oggetti rimangono in memoria anche dopo che non sono più utilizzati.

Esempio:

class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
        print("\(name) viene inizializzato")
    }

    deinit {
        print("\(name) viene de-inizializzato")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?

    init(unit: String) {
        self.unit = unit
        print("Appartamento \(unit) viene inizializzato")
    }

    deinit {
        print("Appartamento \(unit) viene de-inizializzato")
    }
}

In questo esempio, se Person ha un riferimento forte a Apartment, e Apartment ha un riferimento forte a Person, al tentativo di liberare questi oggetti, non verranno eliminati, poiché i loro contatori di riferimenti non raggiungono zero.

Come risolvere il problema dei cicli di ritenzione:

Utilizzare riferimenti weak o unowned.

  • weak: Viene usato quando è possibile che l'oggetto collegato diventi nil durante il ciclo di vita di un altro oggetto. Il riferimento diventa automaticamente nil quando l'oggetto a cui si riferisce viene rilasciato. È opzionale.
  • unowned: Viene usato quando si sa che l'oggetto collegato ha la stessa o maggiore durata di vita rispetto all'oggetto che lo possiede. Non può essere nil.

Esempio corretto usando weak:

Modifichiamo il riferimento tenant nella classe Apartment in modo debole:

class Apartment {
    let unit: String
    weak var tenant: Person? // Usando weak

    init(unit: String) {
        self.unit = unit
        print("Appartamento \(unit) viene inizializzato")
    }

    deinit {
        print("Appartamento \(unit) viene de-inizializzato")
    }
}

Ora, quando l'oggetto Person viene rilasciato, il riferimento tenant in Apartment diventa automaticamente nil, interrompendo il ciclo di ritenzione e consentendo anche a Apartment di essere rilasciato.

Tabella di confronto tra weak e unowned:

Caratteristica weak unowned
Tipo di riferimento Opzionale Non opzionale
Può essere nil? Sì, quando l'oggetto viene rilasciato No
Quando usarlo Quando l'oggetto collegato può diventare nil Quando si sa che l'oggetto non sarà nil
Performance Un po' più lento a causa del controllo nil Un po' più veloce

Inoltre, i cicli di ritenzione possono verificarsi anche nelle chiusure (closures).

Quando una chiusura cattura proprietà dell'istanza o self, può creare un riferimento forte all'oggetto. Se anche questo oggetto ha un riferimento forte alla chiusura (ad esempio, tramite una proprietà di chiusura o delegato), si verifica un ciclo.

Come risolvere i cicli di ritenzione nelle chiusure:

Utilizzare lista di cattura (capture list).

class SomeClass {
    var value = 0

    func doSomethingDelayed() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in // Lista di cattura con weak self
            guard let self = self else { return } // Controllo `nil` per weak self
            self.value += 1
            print("Il valore ora è \(self.value)")
        }
    }
}

In questo esempio, [weak self] crea una referenza debole a self all'interno della chiusura, prevenendo un ciclo di ritenzione.