wyciek pamięci występuje w aplikacji iOS, gdy pamięć, która nie jest już używana, nie jest prawidłowo usuwana i nadal zajmuje miejsce. Może to zaszkodzić wydajności aplikacji, a ostatecznie, gdy aplikacja zabraknie dostępnej pamięci, spowoduje awarię. Aby lepiej zrozumieć, w jaki sposób dochodzi do wycieków pamięci, ważne jest, aby najpierw wiedzieć, w jaki sposób Aplikacje iOS zarządzają pamięcią. Przyjrzyjmy się, jak zapobiegać wyciekom pamięci w szybkich zamknięciach.

automatyczne zliczanie odniesień

w systemie iOS pamięć dzielona jest zarządzana przez każdy obiekt, który śledzi liczbę innych obiektów, które mają do niej odniesienie. Gdy liczba odniesień osiągnie 0, co oznacza, że nie ma więcej odniesień do obiektu, można ją bezpiecznie wyczyścić z pamięci. Jak sama nazwa wskazuje, zliczanie referencji jest konieczne tylko dla typów referencyjnych, podczas gdy typy wartości nie wymagają zarządzania pamięcią. W przeszłości zliczanie referencji było wykonywane ręcznie przez wywołanie retain obiektu w celu zwiększenia jego liczby referencji, a następnie wywołanie release obiektu w celu zmniejszenia jego liczby referencji. Kod ten był prawie w całości, nudny w pisaniu i podatny na błędy. Tak jak większość zadań podrzędnych, stał się zautomatyzowany poprzez automatyczne liczenie referencji (ARC), co sprawia, że niezbędne zachowanie i zwolnienie połączeń w Twoim imieniu w czasie kompilacji.

chociaż ARC w większości zmniejszył potrzebę martwienia się o zarządzanie pamięcią, nadal może powodować szybkie wycieki pamięci, gdy tylko pojawią się okrągłe odwołania. Na przykład, powiedzmy, że mamy klasę Person, która ma właściwość dla klasy apartment, A Klasa Apartment ma właściwość Person o nazwie tenant:

class Person { var name: String var age: Int init(name: String, age: Int) {…} var apartment: Apartment?}class Apartment { let unit: String init(unit: String) {…} var tenant: Person?}let person = Person(name: "Bob", age: 30)let unit4A = Apartment(unit: "4A")person.apartment = unit4Aunit4A.tenant = person

kiedy tworzymy nowe Person i Apartment i przypisujemy je do swoich właściwości, odnoszą się one teraz do siebie w okrągły sposób. Odwołania okrężne mogą również występować z więcej niż dwoma obiektami. W tej sytuacji zarówno Person, jak i Apartment trzymają odniesienie do drugiego, więc w tym przypadku żaden z nich nigdy nie zejdzie do liczby odniesień równej 0 i utrzyma się w pamięci, mimo że żadne inne obiekty nie mają odniesień do żadnego z nich. Jest to znane jako cykl zatrzymania i powoduje wyciek pamięci.

Related: trzy cechy, które byłyby naprawdę szybkie

sposób, w jaki ARC radzi sobie z cyklami retain, polega na tym, że mają różne rodzaje odniesień: silne, słabe i nieaktywne. Silne odniesienia to takie, o których już mówiliśmy, a te odniesienia zwiększają liczbę referencji obiektu. Z drugiej strony, słabe referencje, wciąż dając Ci odniesienie do obiektu, nie zwiększają jego liczby odniesień. Więc gdybyśmy mieli wziąć właściwość tenant na Apartment i uczynić ją słabą referencją, przerwałoby to nasz cykl zachowywania:

class Apartment { let unit: String init(unit: String) {…} weak var tenant: Person?}

teraz nasz Person obiekt nadal trzyma swój Apartment w pamięci, ale odwrotność nie jest już prawdziwa. Tak więc, gdy zniknie Ostatnie silne odniesienie do Person, jego liczba referencji spadnie do 0 i zwolni swój Apartment, którego liczba referencji zostanie również zmniejszona do 0, a oba mogą być poprawnie wyczyszczone z pamięci.

w języku Swift słabe referencje muszą być opcjonalne var s, ponieważ jeśli nie bierzesz odpowiedzialności za przechowywanie obiektu w pamięci, nie możesz zagwarantować, że obiekt nie zmieni się lub nie opuści pamięci. To tutaj wchodzi w grę trzeci rodzaj odniesienia. odwołania unowned są jak słabe odwołania, z wyjątkiem tego, że mogą być nieobowiązkowe let s, ale powinny być używane tylko wtedy, gdy masz pewność, że obiekt nigdy nie powinien być nil. Podobnie jak Wymuś rozpakowane opcje, mówisz kompilatorowi „nie martw się, zajmę się tym. Zaufaj mi.”Ale podobnie jak słabe odniesienia, odniesienia unowned nie robi nic, aby utrzymać obiekt w pamięci, a jeśli opuści pamięć i spróbujesz uzyskać do niego dostęp, Twoja aplikacja ulegnie awarii. Znowu, tak jak siła rozpakowanych opcji.

podczas gdy cykle ZATRZYMANIA są łatwe do zauważenia, gdy dwa obiekty skierowane są na siebie, są trudniejsze do zauważenia, gdy zaangażowane są zamknięcia w Swift, i to jest miejsce, w którym widziałem większość cykli zatrzymania.

unikanie zatrzymywania cykli w zamknięciach

ważne jest, aby pamiętać, że zamknięcia są typami referencyjnymi w języku Swift i mogą powodować zatrzymywanie cykli równie łatwo, jeśli nie łatwiej, jak Klasy. Aby zamknięcie zostało wykonane później, musi zachować wszystkie zmienne, których potrzebuje do uruchomienia. Podobnie jak w przypadku klas, zamknięcie domyślnie przechwytuje odwołania jako silne. Cykl retain z zamknięciem wyglądałby mniej więcej tak:

class SomeObject { var aClosure = { self.doSomething() } ...}

powiązane: kiedy należy używać bloków i zamknięć lub delegatów do wywołań zwrotnych?

nowe wezwanie do działania

w tym przypadku Klasa SomeObject ma silne odniesienie do aClosure, a aClosure również przechwyciła self (instancję SomeObject). To dlatego Swift zawsze sprawia, że dodajesz self. jawnie podczas zamykania, aby zapobiec przypadkowemu przechwyceniu self, nie zdając sobie z tego sprawy, a tym samym najprawdopodobniej powodując cykl retain.

aby mieć zmienne przechwytywania zamknięcia jako słabe lub nieaktywne, możesz podać instrukcje zamknięcia dotyczące przechwytywania niektórych zmiennych:

class SomeObject { var aClosure = { in self.doSomething() delegate?.doSomethingElse() } ...}

więcej o składni zamknięcia tutaj.

większość przykładów zamknięcia, które widziałem w samouczkach lub przykładach, wydaje się przechwytywać self jako unowned i nazwać to dniem, ponieważ przechwytywanie self jako słabe czyni go opcjonalnym, co może być bardziej niewygodne w pracy. Ale jak dowiedzieliśmy się wcześniej, jest to z natury niebezpieczne, ponieważ nastąpi awaria, jeśli self nie jest już w pamięci. Nie różni się to zbytnio od wymuszania rozpakowywania wszystkich opcjonalnych zmiennych, ponieważ nie chcesz wykonywać pracy, aby je bezpiecznie rozpakować. O ile nie możesz być pewien, że self będzie w pobliżu tak długo, jak twoje zamknięcie jest, powinieneś spróbować uchwycić go słabym. Jeśli potrzebujesz nieobowiązkowego siebie wewnątrz zamknięcia, rozważ użycie if let lub guard let, aby uzyskać silny, nieobowiązkowy self wewnątrz zamknięcia. Ponieważ wprowadziłeś to nowe, silne odniesienie wewnątrz zamknięcia Swift, nie będziesz tworzyć cyklu retain, ponieważ odniesienie to zostanie zwolnione na końcu zamknięcia:

var aClosure = { in if let strongSelf = self { doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf) }}

albo nawet lepiej:

var aClosure = { in guard let strongSelf = self else { return } doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf)}

Przechwytywanie siebie mocno

chociaż dobrą praktyką jest przechwytywanie self słabo w zamknięciach, nie zawsze jest to konieczne. Zamknięcia, które nie są zatrzymywane przez self, mogą go uchwycić silnie, nie powodując cyklu zatrzymywania. Kilka typowych przykładów to:

praca z DispatchQueue s W GCD

DispatchQueue.main.async { self.doSomething() // Not a retain cycle}

praca z UIView.animate(withDuration:)

UIView.animate(withDuration: 1) { self.doSomething() // Not a retain cycle}

pierwszy nie jest cyklem retain, ponieważ self nie zachowuje singletonu DispatchQueue.main. W drugim przykładzie UIView.animate(withDuration:) jest metodą klasową, która self również nie ma udziału w zachowywaniu.

Przechwytywanie self mocno w takich sytuacjach nie spowoduje cyklu retain, ale może też nie być tym, czego chcesz. Na przykład powrót do GCD:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { self.doSomething()}

to zamknięcie nie będzie działać przez kolejne 60 sekund i zachowa self, dopóki tego nie zrobi. Może to być zachowanie, które chcesz, ale jeśli chcesz, aby self mógł opuścić pamięć w tym czasie, lepiej byłoby przechwycić go słabo i uruchomić tylko wtedy, gdy self nadal jest w pobliżu:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { in self?.doSomething()}

Innym ciekawym miejscem, gdzie self nie musiałby być mocno przechwytywany, jest lazy zmienne, które nie są zamknięciami, ponieważ będą uruchamiane raz (lub nigdy), a następnie uwalniane:

lazy var fullName = { return self.firstName + " " + self.lastName }()

jeśli jednak zmienna lazy jest zamknięciem, musiałaby słabo uchwycić siebie. Dobrym tego przykładem jest przewodnik po języku programowania Swift:

class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { in if let text = self.text { return "<(self.name)>(text)</(self.name)>" } else { return "<(self.name) />" } }}

jest to również dobry przykład instancji, która jest rozsądna, aby użyć odwołania unowned, ponieważ zamknięcie asHTML powinno istnieć tak długo, jak działa HTMLElement, ale już nie.

TL;DR

podczas pracy z zamknięciami w języku Swift pamiętaj o tym, jak przechwytywasz zmienne, szczególnie selfs. Jeśli self zachowuje zamknięcie w jakikolwiek sposób, upewnij się, że przechwyciłeś je słabo. Przechwytuj zmienne jako unowned tylko wtedy, gdy możesz być pewien, że będą one w pamięci po uruchomieniu zamknięcia, nie tylko dlatego, że nie chcesz pracować z opcjonalnym self. Pomoże to zapobiec wyciekom pamięci w szybkich zamknięciach, co prowadzi do lepszej wydajności aplikacji.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.