Sobes.tech
Middle

Jak działa automatyczne zarządzanie pamięcią (ARC) w Swift?

sobes.tech AI

Odpowiedź od AI

ARC (Automatic Reference Counting) w Swift śledzi i zarządza użyciem pamięci przez Twoje aplikacje. Automatycznie zwalnia pamięć używaną przez instancje klas, gdy nie są już potrzebne, co zapobiega wyciekom pamięci.

Podczas tworzenia nowej instancji klasy, ARC przydziela blok pamięci na przechowywanie informacji o tej instancji. Gdy instancja nie jest już potrzebna, ARC zwalnia tę pamięć, aby mogła być użyta do innych celów.

ARC działa, zliczając liczbę silnych odniesień (strong references) do każdej instancji klasy. Silne odniesienie to odwołanie, które utrzymuje instancję w pamięci. Gdy liczba silnych odniesień do instancji spada do zera, ARC zwalnia z nią powiązaną pamięć.

W Swift istnieją trzy typy odniesień wpływających na ARC:

  1. Silne odniesienia (Strong References): Zwiększają licznik odniesień do instancji.
  2. Słabe odniesienia (Weak References): Nie zwiększają licznika odniesień. Używane, aby uniknąć cykli silnych odniesień, gdy dwa obiekty klasy trzymają się nawzajem silnymi odniesieniami. Słabe odniesienie automatycznie staje się nil podczas deinitializacji obiektu, na który się odwołuje.
  3. Niezaopiekowane odniesienia (Unowned References): Również nie zwiększają licznika odniesień. Używane, gdy jesteśmy pewni, że obiekt, na który się odwołuje, zawsze będzie istniał, dopóki istnieje bieżący obiekt. Nie mogą być opcjonalne (nil). Próba dostępu do zdeinitializowanego obiektu przez niezaopiekowane odniesienie spowoduje błąd wykonania.

Przykład cyklu silnych odniesień:

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")
    }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil // Person nie jest deinitializowany
unit4A = nil // Apartment nie jest deinitializowane

Aby rozwiązać problem cyklu, użyj słabych lub niezaopiekowanych odniesień. W tym przypadku, tenant w Apartment powinien być słabym odniesieniem:

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
    weak var tenant: Person? // Użycie weak reference

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

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

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john?.apartment = unit4A
unit4A?.tenant = john

john = nil // Person jest deinitializowany
unit4A = nil // Apartment jest deinitializowane

Wybór między słabym a niezaopiekowanym odniesieniem zależy od relacji między obiektami:

  • Weak: Jeśli jeden z obiektów może stać się nil przed deinitializacją drugiego (wartość opcjonalna).
  • Unowned: Jeśli jeden z obiektów zawsze istnieje, dopóki istnieje drugi (wartość nieoptymalizowana).

ARC zarządza również pamięcią używaną przez zamknięcia (closures), które przechwytują instancje klas. Cykl silnych odniesień może wystąpić, gdy zamknięcie przechwytuje instancję klasy, a ta instancja ma również silną referencję do zamknięcia. Aby temu zapobiec, używa się list przechwytywania (capture lists) w zamknięciach.

class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        // Brak listy przechwytywania, możliwy silny cykl przechwytywania
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name)>"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

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

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

paragraph = nil // HTMLElement nie jest deinitializowany

Rozwiązanie z użyciem listy przechwytywania [weak self]:

class AnHTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = { [weak self] in // Użycie weak self
        guard let self = self else { return "" } // Sprawdzenie istnienia self
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name)>"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

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

var paragraph: AnHTMLElement? = AnHTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

paragraph = nil // AnHTMLElement jest deinitializowany

Użycie [unowned self] jest możliwe, jeśli jesteśmy pewni, że instancja klasy (self) nie stanie się nil przed wykonaniem zamknięcia.

ARC znacznie upraszcza zarządzanie pamięcią w Swift w porównaniu do ręcznego zarządzania, ale wymaga zrozumienia typów odniesień, aby zapobiec wyciekom pamięciowym.