Sobes.tech
Middle

Czym są cykle zatrzymania (retain cycles) w iOS?

sobes.tech AI

Odpowiedź od AI

Cykl zatrzymania występuje, gdy dwa lub więcej obiektów ma silne odniesienia do siebie, tworząc "zamknięty krąg". W wyniku tego licznik odniesień każdego obiektu nigdy nie osiąga zera i nie może zostać zwolniony przez ARC (Automatyczne Liczenie Odwołań). To prowadzi do wycieków pamięci, ponieważ obiekty pozostają w pamięci nawet po tym, jak przestają być używane.

Przykład:

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

    init(name: String) {
        self.name = name
        print("\(name) jest inicjalizowany")
    }

    deinit {
        print("\(name) jest deinitializowany")
    }
}

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

    init(unit: String) {
        self.unit = unit
        print("Mieszkanie \(unit) jest inicjalizowane")
    }

    deinit {
        print("Mieszkanie \(unit) jest deinitializowane")
    }
}

W tym przykładzie, jeśli Person ma silne odniesienie do Apartment, a Apartment ma silne odniesienie do Person, to podczas próby zwolnienia tych obiektów nie zostaną one usunięte, ponieważ ich liczniki odniesień nie osiągną zera.

Jak rozwiązać problem cykli zatrzymania:

Użyj odniesień weak lub unowned.

  • weak: Używa się, gdy możliwe jest, że powiązany obiekt stanie się nil w trakcie cyklu życia innego obiektu. Odniesienie staje się automatycznie nil, gdy obiekt, na który się odwołuje, zostanie zwolniony. Jest opcjonalne.
  • unowned: Używa się, gdy wiadomo, że powiązany obiekt ma ten sam lub dłuższy cykl życia niż obiekt, który go posiada. Nie może być nil.

Przykład poprawiony z użyciem weak:

Zmienimy odniesienie tenant w klasie Apartment na słabe:

class Apartment {
    let unit: String
    weak var tenant: Person? // Używamy weak

    init(unit: String) {
        self.unit = unit
        print("Mieszkanie \(unit) jest inicjalizowane")
    }

    deinit {
        print("Mieszkanie \(unit) jest deinitializowane")
    }
}

Teraz, gdy obiekt Person zostanie zwolniony, odniesienie tenant w Apartment automatycznie stanie się nil, przerywając cykl zatrzymania i pozwalając na zwolnienie Apartment.

Tabela porównawcza weak i unowned:

Cecha weak unowned
Typ odniesienia Opcjonalny Nie opcjonalny
Może być nil? Tak, gdy obiekt jest zwalniany Nie
Kiedy używać Gdy obiekt może stać się nil Gdy wiadomo, że obiekt nie będzie nil
Wydajność Nieco wolniejszy z powodu sprawdzania nil Nieco szybszy

Ponadto, cykle zatrzymania mogą wystąpić również w zamknięciach (closures).

Gdy zamknięcie przechwytuje właściwości instancji lub self, może utworzyć silne odniesienie do obiektu. Jeśli ten obiekt ma również silne odniesienie do zamknięcia (np. przez właściwość zamknięcia lub delegata), powstaje cykl.

Jak rozwiązać problem cykli zatrzymania w zamknięciach:

Użyj listy przechwytywania (capture list).

class SomeClass {
    var value = 0

    func doSomethingDelayed() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in // Lista przechwytywania z weak self
            guard let self = self else { return } // Sprawdzenie na nil dla weak self
            self.value += 1
            print("Wartość jest teraz \(self.value)")
        }
    }
}

W tym przykładzie, [weak self] tworzy słabe odniesienie do self wewnątrz zamknięcia, zapobiegając cyklowi zatrzymania.