Tym razem zajmę się cyklem życia aplikacji dla systemu iOS. Wiedza na ten temat pomoże Wam lepiej zrozumieć jak działają poszczególne obiekty wchodzące w składa waszych aplikacji. Jest również bardzo prawdopodobne, że pytanie z tym związane pojawi się na potencjalnej rozmowie rekrutacyjnej, ale te założenia opieram jedynie na opiniach programistów z innych zakątków świata, ponieważ na naszym podwórku raczej trudno jest znaleźć konkretne informacje na temat rekrutacji na stanowisko programisty iOS. Tak, czy inaczej wiedza ta okaże się z całą pewnością przydatna 😄.

 

 

Musimy pamiętać, że aplikacje które na co dzień tworzymy stanowią bardzo złożone systemy i zrozumienie szerszego kontekstu pozwoli nam na podejmowanie bardziej świadomych decyzji, które w konsekwencji przełożą się na lepszą wydajność naszego kodu.

Dla łatwiejszej orientacji poniżej macie skrót informacji, które znajdą się w tym wpisie:

  • funkcja main
  • podstawowa struktura aplikacji
  • role poszczególnych obiektów
  • main run loop
  • stany aplikacji

 

 

Funkcja main()

 

 

Każda aplikacja musi mieć jakiś punkt, w którym zaczyna swoje działanie. W przypadku aplikacji dla systemu iOS punktem rozpoczęcia cyklu życia jest funkcja o nazwie main(), która jest tworzona automatycznie przez Xcode podczas przygotowywania naszego projektu. Przykład funkcji, który wziąłem ze strony Apple możecie zobaczyć poniżej (w Objective-C):

 

 

Jaki widać głównym jej zadaniem jest wywołanie kolejnej funkcji o nazwie UIApplicationMain. To właśnie ona będzie odpowiadała za stworzenie struktury całej aplikacji, w tym wywołanie systemowych bibliotek oraz kodu, który sami stworzyliśmy. Uruchamia ona również tzw. pętlę wykonania aplikacji w języku angielskim nazywanąapp run loop”, o której to pętli będzie trochę później. Warto tutaj zaznaczyć, że choć typem zwracanym funkcji jest int to tak na prawdę funkcja nigdy nic nie zwróci, co jest związane właśnie z app run loop.

 

 

Struktura aplikacji

 

 

W początkowej fazie uruchamiania programu funkcja UIApplicationMain wykona za nas całą pracę związaną z tworzeniem podstawowych obiektów, które stanowią trzon naszej aplikacji. Najważniejszym obiektem w całej hierarchii jest UIApplication, który skleja wszystkie pozostałe obiekty w jedną całość i odpowiada za ich komunikację.  Poniżej umieściłem rysunek ze strony Apple, który przedstawia podstawową strukturę aplikacji dla systemu iOS:

 

 

Warto tutaj zwrócić uwagę, że jest to klasyczny przykład podziału obiektów w oparciu o wzorzec projektowy model-view-controller, który stanowi podstawę aplikacji dla iOS’a. MVC (bo takiego skrótu używa się dla tego wzorca) to bardzo szeroki temat, a jego prawidłowa implementacja wymaga odpowiedniej praktyki. MVC ma również konkurencję w postaci MVVM (Model-View-ViewModel). Jeżeli chcecie poczytać trochę więcej o model-view-controller, to możecie zrobić to w tym miejscu.

 

 

Wróćmy jednak na właściwe tory. Poniżej podaję krótką charakterystykę poszczególnych obiektów:

UIApplication –  podstawowy obiekt dla naszej aplikacji – każda aplikacja iOS ma dokładnie jedną instancję tego obiektu – to właśnie funkcja UIApplicationMain odpowiedzialna jest za jego utworzenie (obiekt UIApplication jest Singletonem) – dostęp do niego możemy uzyskać za pomocą statycznego pola o nazwie „shared”. Jest on również odpowiedzialny za informowanie obiektu, który jest jego delegatem, o istotnych fazach w cyklu życia aplikacji – takich jak np. uruchomienie, czy też jej zakończenie. Wyczerpującą dokumentację możecie znaleźć pod tym adresem. UIApplication jest również odpowiedzialny za uruchomienie procesu main run loop.

App delegate – miejsce uruchamiania napisanego przez nas kodu. To jest własnie ten domyślny plik AppDelegate, który jest dodawany automatycznie przez Xcode do każdego projektu.  Można powiedzieć, że jest to punkt startowy, ale z punktu widzenia programisty, a nie systemu 😎. Jest to również jedyny obiekt, który z całą pewnością znajdzie się każdej aplikacji, więc może zostać wykorzystany do początkowej konfiguracji odpowiednich komponentów.

Data objects – są to obiekty przechowujące modele danych. Dobrym przykładem może być tutaj Core Data lub UserDefaults.

View Controller – obiekt, który odpowiada za komunikację pomiędzy Data objects a obiektami generującymi poszczególne widoki na ekranie. Każdy View Controller odpowiada za prezentację pojedynczego widoku głównego wraz z poszczególnymi widokami zależnymi (subviews). Jak się zapewne domyślacie podstawą tego obiekt jest klasa UIViewController.

UIWindow – nie posiada żadnej graficznej reprezentacji (czyli nie jest dla nas w żaden sposób widoczny podczas działania aplikacji), ale jest kluczowy z punktu widzenia elementów UI naszej aplikacji. Jest pierwszym obiektem, który reaguje na gesty wykonywane przez użytkownika, przesyłając następnie odpowiednie informacje do poszczególnych widoków. Nie powinniśmy jednak próbować w żaden sposób wpłynąć na zachowanie obiektu UIWindow, ponieważ jego kontrola leży po stronie systemu. Właściwie jedynym momentem kiedy będziemy z niego korzystać będzie moment jego utworzenia w AppDelegate, o ile zdecydujemy się pracować bez Story Boards. W przeciwnym wypadku obiekt ten zostanie utworzony dla nas automatycznie. Wszelkie zmiany w widoku aplikacji powinniśmy dokonywać za pomocą kontrolera oraz jego widoków zależnych.

View Objects oraz Control Objects – czyli wszystkie nasze przyciski, pola tekstowe i inne podstawowe widoki. Stanowią one wizualną reprezentację naszej aplikacji, jak również są odpowiedzialne za reagowanie na zdarzenia wywołane interakcją ze strony użytkownika.

 

 

Main Run Loop

 

 

Main run loop jest procesem ururchamiany przez obiekt UIApplication na samym początku działania naszej aplikacji. Jest on odpowiedzialny za przetwrzanie wszystkich zdarzeń powstałych w wyniku interakcji użytkownika z aplikacją. Na podstawie tych zdarzeń obiekt UIApplication dokonuje aktualizacji widoków w aplikacji. Main run loop działa na wątku głównym, a co za tym idzie zdarzenia na nim są obsługiwane seryjnie (czyli każde zdarzenie zostanie przetworzone dopiero w momencie zakończenia poprzedniego) zgodnie z zasadami działania kolejki FIFO (first-in forst-out).

Dla lepszego rozeznania poniżej umieściłem grafikę z dokumentacji Apple:

 

Processing events in the main run loop

 

Cały proces wygląda mniej więcej tak:

1. Użytkownik wchodzi w itnterakcję z aplikacją za pomocą jakiegoś gestu (stuknięcie palcem w ekran, przeciągnięcie palcem po ekranie).

2. System przetwarza te gesty na określone informacje (zdarzenia), które następnie przez specjalny port (utwrzony przez bibliotekę UIKit) przesyłane są do aplikacji.

3. Następnie zdarzenia te są ustawiane we wspomnianej wcześniej kolejce FIFO.

4. Z kolejki FIFO zadania przemieszczają się właśnie do main run loop, który to proces jest odpowiedzialny za ich wykonanie.

5. Każde zadanie opuszcza kolejkę dopiero w momencie zakończenia poprzedniego zadania (to jest właśnie główna cecha kolejek seryjnych).

6. Następnie do akcji wchodzi obiekt UIApplication, który decyduje co z danym zdarzeniem należy zrobić.

7. Zdarzenie związane z gestem użytkownika jest najczęściej kierowane do obiektu UIWindow, który z kolei przekazuje je do widoku, z którym użytkownik wszedł w interakcje (to ten widok, który użytkownik dotknął palcem).

8. Sprawa wygląda trochę inaczej kiedy zdarzenie nie jest związane z gestem użytkownika – istnieje wiele różnych zdarzeń, które mogą zostać wysłane do naszej aplikacji – wiele z nich zostanie obsłużone za pomocą main run loop, jednak niektóre z nich zostaną delegowane do obiektów lub samodzielnych bloków kodu, które zostały przez nas zdefiniowane (mowa tutaj o tzw. closures) – przykładem mogą być zdarzenia związane ze zmianą lokalizacji użytkownika, której obsługę możemy zaimplementować za pomocą biblioteki Core Location.

9. Zdarzenia wywołane interakcją użytkownika z aplikacją są obsługiwane przez tzw. responder objects, czyli zbiór obiektów, które „potencjalnie” mogą zająć się danym zdarzeniem. Działa to w ten sposób, że każdy obiekt z puli sprawdza, czy może obsłużyć żądanie i w przypadku negatywnej decyzji przekazuje zdarzenie w dół hierarchii.

10. Do responder objects należą między innymi UIApplication, wszystkie pochodne klasy UIView, a także View Controllers.

To jest oczywiście bardzo ogólny opis i każdy etap w rzeczywistości jest dużo bardziej złożony. To jednak bardzo dobre wprowadzenie, które może stać się dla Was punktem wyjścia do dalszej analizy.

 

 

STany Aplikacji

 

 

W dowolnym momencie działania aplikacja może znajdować się w jednym z 5 stanów, czyli tzw. app execution states. Decyzję o tym w jakim stanie umieścić aplikację podejmuje system na podstawie różnych zdarzeń – może on np. całkowicie zakończyć aplikację, która znajduje się w stanie zawieszonym, gdy okaże się, że zaczyna brakować w urządzeniu pamięci. Przejście do każdego stanu wiąże się również z wywołaniem odpowiedniej metody w obiekcie delegacie naszej aplikacji (najczęściej jest to nasz domyślny obiekt AppDelegate) – metody te pozwalają na wykonanie określonych operacji – np. zapisanie edytowanego dokumentu tuż przed zamknięciem aplikacji lub wykonanie dowolnych prac porządkowych. Poniżej zamieściłem poszczególne stany wraz z ich opisami:

Not running – aplikacja nie została jeszcze uruchomiona lub została zamknięta przez system.

Inactive – aplikacja znajduje się na pierwszym planie (foreground), ale nie przetwarza żadnych zdarzeń. Może jednak wykonywać wcześniej zadany kod. Inactive jest zwykle tylko etapem przejściowym pomiędzy innymi stanami.

Active – aplikacja jest na pierwszym planie (foreground) i przetwarza zdarzenia.

Background – aplikacja jest na drugim planie (background), ale w dalszym ciągu może wykonywać zadane operacje (m.in. nasz kod). Zwykle jest to etap przejściowy przed stanem Suspended – aplikacja może jednak zażądać dodatkowego czasu na wykonanie określonych operacji.

Suspended – aplikacja znajduje się na drugim planie (background), ale nie wykonuje żadnych operacji, w tym naszego kodu. System nie informuje aplikacji, że przenosi ją do tego stanu. Aplikacja w dalszym ciągu znajduje się w pamięci urządzenia, ale jest całkowicie bierna. Kiedy pojawi się informacja o niskich zasobach pamięci (np. inne aktywne aplikacje zaczną ją zajmować) to system może zdecydować o całkowitym usunięciu z pamięci zawieszonej aplikacji.

Warto skorzystać z wiedzy na temat cyklu życia aplikacji, aby zaplanować odpowiednio zadania np. zapisując wszystkie istotne dane tuż przed zamknięciem programu przez system lub użytkownika. System może zdecydować o zamknięciu aplikacji, która nie odpowiada na zdarzenia w przewidzianym czasie lub zachowuje się w inny, nieakceptowany przez system sposób. Zamknięcie aplikacji przez użytkownika ma na nią taki sam wpływa jak zamknięcie jej przez system.

 

 

Słowo na drogę

 

 

Jak sami widzicie temat jest bardzo obszerny i można by jeszcze długo tak pisać. Wiedza na temat cyklu życia aplikacji jest jednak bardzo przydatna, ponieważ pomaga nam lepiej zrozumieć jak działają nasze programy, dzięki czemu stajemy bardziej świadomymi programistami. Nikt nie oczekuje oczywiście, że uda Wam się to wszystko spamiętać, ale warto jest zanotować sobie kilka kluczowych rzeczy, żeby wiedzieć gdzie szukać jak pojawi się taka potrzeba.

Na koniec w punktach wymieniłem jeszcze raz najistotniejsze rzeczy w tym wpisie:

  • funkcja main
  • podstawowa struktura aplikacji
  • role poszczególnych obiektów
  • main run loop
  • stany aplikacji

 


 

Dodaj komentarz

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