Zagadnienia związane z ARC (Automatic Refernce Counting) oraz strong reference cycle przez dłuższy czas spędzały mi sen z powiek, bo jakoś nijak nie mogłem tego wszystkie poukładać sobie w głowie. Niby wszystko wydaje się całkiem proste i logiczne, ale jak już trzeba to przełożyć na działającą aplikację to zaczynają pojawiać się problemy. Żeby ułatwić Wam trochę życie postaram się w prostych słowach wyjaśnić czym właściwie jest ARC oraz jak ustrzec się strong reference cycle. Zaczynamy 😎.

 

 

Swift jest językiem bardzo nowoczesnym i przyjaznym nam – programistom, dlatego większość operacji związanych z zarządzaniem pamięcią (takich jak jej przydzielanie i zwalnianie) wykonuje za nas. Wykorzystuje do tego narzędzie określane w skrócie jako ARC, czyli właśnie Automatic Reference Counting. Jego zrozumienie pomoże Wam we właściwy sposób kontrolować cykl życia obiektu znajdującego się na stercie (heap). ARC nie zajmuje się obiektami typu wartościowego (value types), ponieważ te składowane są na stosie (stack), który zarządzany jest w bardzo wydajny sposób przez CPU.

Opisując ARC w najprostszy możliwy sposób możemy powiedzieć, że jego głównym zadaniem jest zliczanie ilości referencji (czyli odwołań – zwykła zmienna, która przechowuje Wasz obiekt tworzy właśnie takie odwołanie) do danego obiektu w naszej aplikacji. Jeżeli liczba referencji do danego obiektu spadnie do zera to powinien on zostać usunięty z pamięci. W większości wypadków ARC będzie sobie doskonale radził bez naszej pomocy, jednak to my będziemy odpowiedzialni za właściwe określanie relacji pomiędzy obiektami w celu uniknięcia wycieków pamięci (memory leak). Wyciek pamięci występuje wtedy, gdy obiekt, którego już nie potrzebujemy zalega w pamięci, a my nie jesteśmy w stanie odzyskać przydzielonych mu zasobów. Wycieki są o tyle niebezpieczne, że skumulowane mogą doprowadzić do zamknięcia naszej aplikacji w bardzo nieprzyjemny sposób. Użytkownik z całą pewnością nie będzie z tego faktu zadowolony. Musimy również pamiętać, że jak na prawdziwych inżynierów przystało, powinniśmy dbać o każdy najdrobniejszy szczegół w naszej aplikacji 😉.

 

 

 

Tyle tytułem wstępu. Przejdźmy do pisania kodu. Najłatwiej dla Was będzie jak skorzystacie z Playground, którego odpalicie z poziomu menu startowego Xcode.

Wpiszcie na samym początku poniższy kod:

 

 

W bocznym panelu możecie zobaczyć, że wiadomość umieszczona wewnątrz konstruktora została wyświetlona podczas tworzenia nowej instancji obiektu (initialization), a tuż przed jego usunięciem z pamięci (deallocation) wyświetlana jest kolejna wiadomość. W Swift życie każdego obiektu wygląda następująco:

– zajęcie odpowiedniej ilości pamięci (allocation) na stercie (stack) lub stosie

– tworzenie danego obiektu (initialization) – zostaje wykonany kod w bloku init()

– wykorzystanie obiektu w aplikcaji

– usunięcie / zniszczenie obiektu  – zostaje wykonany kod w bloku deinit()

– zwolnienie zajętej pamięci na stercie lub stosie (deallocation)

Kiedy obiekt nie jest już dłużej nikomu potrzebny (w sensie innym obiektom) to powinien zostać usunięty, a zajmowana przez niego pamięć zwolniona. ARC w każdej instancji obiektu przechowuje liczbę referencji (reference count) jakie posiada dany obiekt. Referencje określają ile innych obiektów wykorzystuje daną instancę. To właśnie na podstawie ich ilości ARC podejmuje decyzję, czy należy pozbyć się danego obiektu.

Kiedy tworzycie obiekt Dogi i przypisujecie go do stałej firstDog to liczba referencji instancji wzrasta o 1, ponieważ firstDog odnosi się teraz właśnie do tej instancji. Innymi słowy firstDog robi z tej instancji pewien użytek. W momencie, gdy program kończy wykonywanie instrukcji w bloku do, stała firstDog kończy swój krótki żywot. W tym samy momencie liczba referencji do instancji obiektu Dogi jest zmniejszana o 1, a co za tym idzie obiekt zostaje zniszczony. Zajmowana przez niego pamięć wraca do wspólnej puli. Dzieje się tak właśnie dlatego, że liczba referencji do obiektu spadła do zera, a więc nikt nie miał już z niego żadnego pożytku.

Wszystko działa sobie bardzo dobrze, aż do momentu, gdy przez swoją nieuwagę tworzymy silną referencję pomiędzy dwoma obiektami. To jest właśnie moment powstania wycieku pamięci w naszej aplikacji (memory leak).

 

 

Do naszego przykładu wprowadźcie poniższe zmiany. Nowy kod został odpowiednio zaznaczony:

 

 

Powyższy kod przedstawia tworzenie silnej referencji pomiędzy dwoma obiektami – nasz psiak trzyma referencję do zdobytej kości bo jest jej właścicielem, a sama kość trzyma referencję do psiaka, bo jest jego własnością.

Jeżeli chcemy uniknąć zjawiska strong refernce cycle to musimy określić jedną z relacji pomiędzy obiektami jako weak (słabą) – w wyniku tego licznik referencji nie będzie zwiększony i oba obiekty zostaną poprawnie usunięte z pamięci kiedy nie będą już nikomu potrzebne. Wymaganą zmianę przedstawiłem na poniższym listingu:

 

 

Musicie pamiętać, że weak references zawsze deklarowane są jako typ optional, co oznacza, że obiekt, do którego się odwołują w pewnym momencie może przestać istnieć, a więc zmienna go przechowująca będzie wskazywała na nil (czyli brak wartości). W powyższym przykładzie taki los czeka właśnie właściciela wielkiej kości 😅.

Istnieje jeszcze jeden rodzaj, którym można opisać referencję, a który nie zwiększa licznika strong reference count – jego nazwa to unowned. Różnica pomiędzy weak a unowned polega na tym, że obiekt do którego odwołuje się referencja weak zawsze musi być typu optional (ponieważ może on w którymś momencie zostać zniszczony), natomiast obiekt, do którego odwołuje się referencja typu unowned nie może być typu optional. Dlatego z unowned musicie postępować bardzo ostrożnie. Próba odwołania się do obiektu, który już nie istnieje zakończy się wywołaniem błędu w aplikacji. W przypadku użycia referencji tego typu musicie mieć absolutną pewność, że dany obiekt będzie w dalszym ciągu dostępny kiedy będziecie chcieli z niego skorzystać. W przeciwnym wypadku bezpieczniej będzie użyć referencji typu weak.

 

Strong reference count w closures

 

Z closure (samodzielny blok kodu) jest tak, że obiekt, w którym się znajduje trzyma do niego silną referencję. Z drugiej strony closure wykorzystujące jakieś pole z przykładowego obiektu również odwzajemnia się silną referencją – dzieje się to poprzez uchwyconą przez blok kodu wartość self (tzw. captured value). Taki przypadek prezentuje poniższy przykład:

 

 

Do rozwiązania tego problemu należy wykorzystać tzw. capture list, które Swift wykorzystuje do określenia jaka relacja zachodzi pomiędzy samodzielnym blokiem kodu, a wartościami, które przechwytuje. Najpierw rzucie okiem ten dodatkowy przykład:

 

 

Mówi się, że w tym wypadku wartość johnName jest kopiowana (captured by value) i w bloku zostanie jej przypisana dokładnie ta wartość, którą zmienna johnName posiada w momencie wykonywania jej kopii. Z kolei w przypadku agataName do bloku przekazywana jest jedynie referencja (captured by reference), co oznacza, że w momencie wykonania kodu umieszczonego w closure wartość agataName będzie równa aktualnej wartości zmiennej. To może wydawać się trochę zakręcone, ale szybko złapiecie o co chodzi jak sobie to rozpiszecie w kodzie 🤓.

Za pomocą capture list możemy określić czy relacja pomiędzy blokiem a poszczególnymi obiektami będzie weak czy unowned. W naszym wypadku zdecydujemy się na weak. Tak będzie wyglądał kod po zmianie:

 

 

W tym wypadku kompilator wykona dla na kopię self (tzw. shadow copy), która nie będzie zwiększała reference count dla obiektu Bone. Jednocześnie pozwoli on nam na wykorzystanie kopii w ten sam sposób w jaki korzystalibyśmy z normalnego obiektu.

Jak sami widzicie zagadnienia związane z ARC i zarządzaniem pamięcią w Swfit wydają się być na początku bardzo złożone, ale przy odrobinie praktyki da się to wszystko zrozumieć i odpowiednio zastosować w projektach. To co opisałem jest tylko wstępem. Myślę jednak, że wskaże on Wam właściwy kierunek. Oczywiście jak chcecie dowiedzieć się więcej to musicie zajrzeć do obszernej dokumentacji Apple. Bez dokumentacji ani rusz.  Do usłyszenia 😎.

  


 

Comments

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *