Sztuczki wydajności iOS, które sprawią, że Twoja aplikacja będzie bardziej wydajna
Opublikowany: 2022-03-10Chociaż nowoczesny sprzęt iOS jest wystarczająco wydajny, aby obsłużyć wiele intensywnych i złożonych zadań, urządzenie może nadal nie odpowiadać, jeśli nie uważasz, jak działa Twoja aplikacja. W tym artykule przyjrzymy się pięciu sztuczkom optymalizacyjnym, które sprawią, że Twoja aplikacja będzie bardziej responsywna.
1. Usuń z kolejki komórkę wielokrotnego użytku
Prawdopodobnie wcześniej tableView.dequeueReusableCell(withIdentifier:for:)
wewnątrz tableView(_:cellForRowAt:)
. Czy zastanawiałeś się kiedyś, dlaczego musisz podążać za tym niezręcznym interfejsem API, zamiast po prostu przekazywać tablicę komórek? Przeanalizujmy to rozumowanie.
Załóżmy, że masz widok tabeli z tysiącem wierszy. Bez używania komórek wielokrotnego użytku musielibyśmy utworzyć nową komórkę dla każdego wiersza, w ten sposób:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // Create a new cell whenever cellForRowAt is called. let cell = UITableViewCell() cell.textLabel?.text = "Cell \(indexPath.row)" return cell }
Jak mogłeś pomyśleć, spowoduje to dodanie tysiąca komórek do pamięci urządzenia podczas przewijania w dół. Wyobraź sobie, co by się stało, gdyby każda komórka zawierała UIImageView
i dużo tekstu: załadowanie ich wszystkich naraz może spowodować, że aplikacja zabraknie pamięci! Poza tym każda komórka wymagałaby przydzielenia nowej pamięci podczas przewijania. Jeśli szybko przewiniesz widok tabeli, wiele małych fragmentów pamięci zostanie przydzielonych w locie, a ten proces sprawi, że interfejs użytkownika będzie chybotliwy!
Aby rozwiązać ten problem, firma Apple udostępniła nam dequeueReusableCell(withIdentifier:for:)
. Ponowne użycie komórki polega na umieszczeniu komórki, która nie jest już widoczna na ekranie, w kolejce, a gdy nowa komórka ma być widoczna na ekranie (powiedzmy, że kolejna komórka poniżej, gdy użytkownik przewija w dół), widok tabeli zostanie pobrać komórkę z tej kolejki i zmodyfikować ją w cellForRowAt indexPath:
Używając kolejki do przechowywania komórek, widok tabeli nie musi tworzyć tysiąca komórek. Zamiast tego potrzebuje tylko wystarczającej liczby komórek, aby pokryć obszar widoku tabeli.
Używając dequeueReusableCell
, możemy zmniejszyć pamięć używaną przez aplikację i sprawić, że będzie mniej podatna na jej brak!
2. Korzystanie z ekranu uruchamiania, który wygląda jak ekran początkowy
Jak wspomniano w wytycznych Apple Human Interface Guidelines (HIG), ekrany uruchamiania mogą być używane do poprawy postrzegania responsywności aplikacji:
„Ma ona na celu wyłącznie poprawę postrzegania Twojej aplikacji jako szybkiej do uruchomienia i natychmiastowej gotowości do użycia. Każda aplikacja musi zawierać ekran startowy”.
Częstym błędem jest używanie ekranu uruchamiania jako ekranu powitalnego w celu pokazania marki lub dodania animacji wczytywania. Zaprojektuj ekran uruchamiania tak, aby był identyczny z pierwszym ekranem Twojej aplikacji, jak wspomniał Apple:
„Zaprojektuj ekran uruchamiania, który jest prawie identyczny z pierwszym ekranem Twojej aplikacji. Jeśli dodasz elementy, które wyglądają inaczej po zakończeniu uruchamiania aplikacji, ludzie mogą doświadczyć nieprzyjemnego błysku między ekranem uruchamiania a pierwszym ekranem aplikacji.
„Ekran startowy nie jest okazją do budowania marki. Nie projektuj wejścia, które wygląda jak ekran powitalny lub okno „Informacje”. Nie umieszczaj logo ani innych elementów marki, chyba że są one statyczną częścią pierwszego ekranu Twojej aplikacji”.
Używanie ekranu startowego do ładowania lub brandingu może spowolnić czas pierwszego użycia i sprawić, że użytkownik poczuje, że aplikacja działa wolno.
Po rozpoczęciu nowego projektu iOS zostanie utworzony pusty LaunchScreen.storyboard
. Ten ekran zostanie wyświetlony użytkownikowi, gdy aplikacja ładuje kontrolery widoku i układ.
Aby aplikacja działała szybciej, możesz zaprojektować ekran uruchamiania podobny do pierwszego ekranu (kontrolera widoku), który będzie wyświetlany użytkownikowi.
Na przykład ekran uruchamiania aplikacji Safari jest podobny do pierwszego widoku :
Scenorys ekranu uruchamiania jest jak każdy inny plik scenorysu, z tą różnicą, że można używać tylko standardowych klas UIKit, takich jak UIViewController, UITabBarController i UINavigationController. Jeśli spróbujesz użyć innych niestandardowych podklas (takich jak UserViewController), Xcode powiadomi Cię, że używanie niestandardowych nazw klas jest zabronione.
Inną rzeczą, na którą należy zwrócić uwagę, jest to, że UIActivityIndicatorView
nie jest animowany po umieszczeniu na ekranie uruchamiania, ponieważ iOS wygeneruje statyczny obraz ze scenorysu ekranu uruchamiania i wyświetli go użytkownikowi. (Wspomina o tym krótko w prezentacji WWDC 2014 „Platforms State of the Union”, około 01:21:56
).
Firma HIG firmy Apple radzi nam również, aby nie umieszczać tekstu na ekranie uruchamiania, ponieważ ekran uruchamiania jest statyczny i nie można zlokalizować tekstu w celu dostosowania do różnych języków.
Zalecana literatura : Aplikacja mobilna z funkcją rozpoznawania twarzy: jak to urzeczywistnić
3. Przywracanie stanu dla kontrolerów widoku
Zachowywanie i przywracanie stanu umożliwia użytkownikowi powrót do dokładnie tego samego stanu interfejsu użytkownika tuż przed opuszczeniem aplikacji. Czasami, z powodu niewystarczającej ilości pamięci, system operacyjny może wymagać usunięcia aplikacji z pamięci, gdy aplikacja działa w tle, a aplikacja może stracić śledzenie ostatniego stanu interfejsu użytkownika, jeśli nie zostanie zachowana, co może spowodować utratę pracy użytkowników w trakcie!
Na ekranie wielozadaniowości możemy zobaczyć listę aplikacji, które zostały umieszczone w tle. Możemy założyć, że te aplikacje nadal działają w tle; w rzeczywistości niektóre z tych aplikacji mogą zostać zabite i ponownie uruchomione przez system ze względu na zapotrzebowanie na pamięć. Migawki aplikacji, które widzimy w widoku wielozadaniowości, to w rzeczywistości zrzuty ekranu wykonane przez system od razu po wyjściu z aplikacji (tj. Aby przejść do ekranu głównego lub ekranu wielozadaniowości).
iOS używa tych zrzutów ekranu, aby dać złudzenie, że aplikacja nadal działa lub nadal wyświetla ten konkretny widok, podczas gdy aplikacja mogła już zostać zamknięta lub ponownie uruchomiona w tle, jednocześnie wyświetlając ten sam zrzut ekranu.
Czy zdarzyło Ci się kiedyś, po wznowieniu aplikacji z ekranu wielozadaniowości, że aplikacja wyświetla interfejs użytkownika inny niż migawka pokazana w widoku wielozadaniowości? Dzieje się tak, ponieważ aplikacja nie zaimplementowała mechanizmu przywracania stanu, a wyświetlane dane zostały utracone, gdy aplikacja została zabita w tle. Może to prowadzić do złych doświadczeń, ponieważ użytkownik oczekuje, że Twoja aplikacja będzie w takim samym stanie, w jakim ją opuścił.
Z artykułu Apple:
„Oczekują, że Twoja aplikacja będzie w takim samym stanie, w jakim ją opuścili. Zachowywanie i przywracanie stanu gwarantuje, że Twoja aplikacja powróci do poprzedniego stanu po ponownym uruchomieniu”.
UIKit wykonuje dużo pracy, aby uprościć zachowywanie i przywracanie stanu: automatycznie obsługuje zapisywanie i ładowanie stanu aplikacji w odpowiednich momentach. Wszystko, co musimy zrobić, to dodać konfigurację, aby poinformować aplikację, aby obsługiwała zachowywanie i przywracanie stanu, a także powiedzieć aplikacji, jakie dane należy zachować.
Aby umożliwić zapisywanie i przywracanie stanu, możemy zaimplementować te dwie metody w AppDelegate.swift
:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
To powie aplikacji, aby automatycznie zapisywała i przywracała stan aplikacji.
Następnie powiemy aplikacji, które kontrolery widoku należy zachować. Robimy to, określając „Identyfikator przywracania” w scenorysie:
Możesz także zaznaczyć "Użyj identyfikatora scenorysu", aby użyć identyfikatora scenorysu jako identyfikatora przywrócenia.
Aby ustawić identyfikator przywrócenia w kodzie, możemy użyć właściwości restorationIdentifier
kontrolera widoku.
// ViewController.swift self.restorationIdentifier = "MainVC"
Podczas zachowywania stanu każdy kontroler widoku lub widok, któremu przypisano identyfikator przywracania, zostanie zapisany na dysku.
Identyfikatory przywracania mogą być grupowane w celu utworzenia ścieżki przywracania. Identyfikatory są pogrupowane przy użyciu hierarchii widoków, od głównego kontrolera widoku do bieżącego aktywnego kontrolera widoku. Załóżmy, że MyViewController jest osadzony w kontrolerze nawigacji, który jest osadzony w innym kontrolerze paska kart. Zakładając, że używają własnych nazw klas jako identyfikatorów przywracania, ścieżka przywracania będzie wyglądać tak:
TabBarController/NavigationController/MyViewController
Gdy użytkownik opuści aplikację z MyViewController będącym aktywnym kontrolerem widoku, ta ścieżka zostanie zapisana przez aplikację; wtedy aplikacja zapamięta poprzednią pokazaną hierarchię widoków ( Kontroler paska zakładek → Kontroler nawigacji → Mój kontroler widoku ).
Po przypisaniu identyfikatora przywrócenia będziemy musieli zaimplementować metody encodeRestorableState(z koderem:) i decodeRestorableState(z koderem:) dla każdego z zachowanych kontrolerów widoku. Te dwie metody pozwalają nam określić, jakie dane należy zapisać lub wczytać oraz jak je zakodować lub zdekodować.
Zobaczmy kontroler widoku:
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { // will be called during state preservation override func encodeRestorableState(with coder: NSCoder) { // encode the data you want to save during state preservation coder.encode(self.username, forKey: "username") super.encodeRestorableState(with: coder) } // will be called during state restoration override func decodeRestorableState(with coder: NSCoder) { // decode the data saved and load it during state restoration if let restoredUsername = coder.decodeObject(forKey: "username") as? String { self.username = restoredUsername } super.decodeRestorableState(with: coder) } }
Pamiętaj, aby wywołać implementację nadklasy na dole własnej metody. Gwarantuje to, że klasa nadrzędna ma szansę na zapisanie i przywrócenie stanu.
Po zakończeniu dekodowania obiektów zostanie wywołana funkcja applicationFinishedRestoringState()
w celu poinformowania kontrolera widoku, że stan został przywrócony. W tej metodzie możemy zaktualizować interfejs użytkownika dla kontrolera widoku.
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { ... override func applicationFinishedRestoringState() { // update the UI here self.usernameLabel.text = self.username } }
Masz to! Są to podstawowe metody implementacji zachowywania i przywracania stanu Twojej aplikacji. Pamiętaj, że system operacyjny usunie zapisany stan, gdy użytkownik wymusi zamknięcie aplikacji, aby uniknąć utknięcia w stanie zepsutym na wypadek, gdyby coś poszło nie tak podczas zachowywania i przywracania stanu.
Ponadto nie przechowuj żadnych danych modelu (tj. danych, które powinny zostać zapisane w UserDefaults lub Core Data) do stanu, nawet jeśli może się to wydawać wygodne. Dane stanu zostaną usunięte, gdy użytkownik wymusza zamknięcie Twojej aplikacji i na pewno nie chcesz w ten sposób utracić danych modelu.
Aby sprawdzić, czy zachowywanie i przywracanie stanu działa prawidłowo, wykonaj poniższe czynności:
- Twórz i uruchamiaj aplikację za pomocą Xcode.
- Przejdź do ekranu z zachowaniem i przywracaniem stanu, który chcesz przetestować.
- Wróć do ekranu głównego (przesuwając palcem w górę lub klikając dwukrotnie przycisk główny albo naciskając Shift ⇧ + Cmd ⌘ + H w symulatorze), aby wysłać aplikację w tło.
- Zatrzymaj aplikację w Xcode, naciskając przycisk.
- Uruchom aplikację ponownie i sprawdź, czy stan został pomyślnie przywrócony.
Ponieważ ta sekcja obejmuje tylko podstawy zachowania i przywracania stanu, polecam następujące artykuły firmy Apple Inc., aby uzyskać bardziej dogłębną wiedzę na temat przywracania stanu:
- Zachowywanie i przywracanie stanu
- Proces zachowywania interfejsu użytkownika
- Proces przywracania interfejsu użytkownika
4. Ogranicz użycie nieprzezroczystych widoków tak bardzo, jak to możliwe
Widok nieprzezroczysty to widok, który nie ma przezroczystości, co oznacza, że żaden element interfejsu użytkownika umieszczony za nim nie jest w ogóle widoczny. Możemy ustawić nieprzezroczysty widok w Interface Builder:
Lub możemy to zrobić programowo za pomocą właściwości isOpaque
UIView:
view.isOpaque = true
Ustawienie widoku na nieprzezroczysty spowoduje, że system rysowania zoptymalizuje wydajność rysowania podczas renderowania ekranu.
Jeśli widok ma przezroczystość (tzn. alfa jest poniżej 1,0), iOS będzie musiał wykonać dodatkową pracę, aby obliczyć, co powinno być wyświetlane, łącząc różne warstwy widoków w hierarchii widoków. Z drugiej strony, jeśli widok jest ustawiony jako nieprzezroczysty, system rysowania po prostu umieści ten widok z przodu i uniknie dodatkowej pracy związanej z mieszaniem wielu warstw widoków znajdujących się za nim.
Możesz sprawdzić, które warstwy są mieszane (nieprzezroczyste) w symulatorze iOS, zaznaczając Debug → Color Blended Layers .
Po zaznaczeniu opcji Color Blended Layers można zauważyć, że niektóre widoki są czerwone, a inne zielone. Kolor czerwony oznacza, że widok nie jest nieprzezroczysty i że jego wyświetlanie wyjściowe jest wynikiem zmieszania się za nim warstw. Zielony oznacza, że widok jest nieprzezroczysty i nie wykonano mieszania.
Etykiety pokazane powyżej („Wyświetl znajomych” itp.) Są podświetlone na czerwono, ponieważ po przeciągnięciu etykiety do scenorysu jej kolor tła jest domyślnie ustawiony jako przezroczysty. Kiedy system rysujący komponuje wyświetlacz w pobliżu obszaru etykiety, poprosi o warstwę za etykietą i wykona pewne obliczenia.
Jednym ze sposobów optymalizacji wydajności aplikacji jest maksymalne ograniczenie liczby widoków wyróżnionych na czerwono.
Zmieniając label.backgroundColor = UIColor.clear
na label.backgroundColor = UIColor.white
, możemy zmniejszyć przenikanie warstw między etykietą a warstwą widoku znajdującą się za nią.
Być może zauważyłeś, że nawet jeśli ustawisz UIImageView na nieprzezroczysty i przypisano mu kolor tła, symulator nadal będzie wyświetlał kolor czerwony w widoku obrazu. Dzieje się tak prawdopodobnie dlatego, że obraz użyty w widoku obrazu ma kanał alfa.
Aby usunąć kanał alfa dla obrazu, możesz użyć aplikacji Podgląd, aby utworzyć duplikat obrazu ( Shift ⇧ + Cmd ⌘ + S ) i odznacz pole wyboru „Alfa” podczas zapisywania.
5. Przekaż ciężkie funkcje przetwarzania do wątków w tle (GCD)
Ponieważ UIKit działa tylko w głównym wątku, intensywne przetwarzanie w głównym wątku spowolni interfejs użytkownika. Główny wątek jest używany przez UIKit nie tylko do obsługi i reagowania na dane wejściowe użytkownika, a także do rysowania ekranu.
Kluczem do tego, aby aplikacja była responsywna, jest przeniesienie jak największej liczby ciężkich zadań przetwarzania do wątków w tle. Unikaj wykonywania złożonych obliczeń, pracy w sieci i ciężkich operacji we/wy (np. odczytywanie i zapisywanie na dysku) w głównym wątku.
Być może kiedyś korzystałeś z aplikacji, która nagle przestała reagować na wprowadzanie dotykowe i wydaje się, że aplikacja się zawiesiła. Jest to najprawdopodobniej spowodowane przez aplikację wykonującą ciężkie zadania obliczeniowe w głównym wątku.
Główny wątek zwykle zmienia się między zadaniami UIKit (takimi jak obsługa danych wejściowych użytkownika) i niektórymi zadaniami lżejszymi w małych odstępach czasu. Jeśli w głównym wątku działa ciężkie zadanie, UIKit będzie musiał poczekać, aż ciężkie zadanie zostanie zakończone, zanim będzie mógł obsłużyć wprowadzanie dotykowe.
Domyślnie kod wewnątrz metod cyklu życia kontrolera widoku (takich jak viewDidLoad) i funkcje IBOutlet są wykonywane w głównym wątku. Aby przenieść ciężkie zadania przetwarzania do wątku w tle, możemy użyć kolejek Grand Central Dispatch dostarczanych przez Apple.
Oto szablon do przełączania kolejek:
// Switch to background thread to perform heavy task. DispatchQueue.global(qos: .default).async { // Perform heavy task here. // Switch back to main thread to perform UI-related task. DispatchQueue.main.async { // Update UI. } }
qos
oznacza „jakość usług”. Różne wartości jakości usług wskazują różne priorytety dla określonych zadań. System operacyjny przydzieli więcej czasu procesora i przepustowość wejścia/wyjścia mocy procesora dla zadań przydzielonych w kolejkach o wyższych wartościach QoS, co oznacza, że zadanie zakończy się szybciej w kolejce z wyższymi wartościami QoS. Wyższa wartość QoS będzie również zużywać więcej energii, ponieważ zużywa więcej zasobów.
Oto lista wartości QoS od najwyższego do najniższego priorytetu:
Firma Apple udostępniła przydatną tabelę z przykładami wartości QoS do wykorzystania w różnych zadaniach.
Należy pamiętać, że cały kod UIKit powinien być zawsze wykonywany w głównym wątku. Modyfikowanie obiektów UIKit (takich jak UILabel
i UIImageView
) w wątku w tle może mieć niezamierzone konsekwencje, takie jak brak aktualizacji interfejsu użytkownika, wystąpienie awarii i tak dalej.
Z artykułu Apple:
„Aktualizacja interfejsu użytkownika w wątku innym niż wątek główny jest częstym błędem, który może skutkować pominiętymi aktualizacjami interfejsu użytkownika, defektami wizualnymi, uszkodzeniem danych i awariami”.
Polecam obejrzeć film Apple WWDC 2012 na temat współbieżności interfejsu użytkownika, aby lepiej zrozumieć, jak zbudować responsywną aplikację.
Uwagi
Kompromis z optymalizacji wydajności polega na tym, że musisz napisać więcej kodu lub skonfigurować dodatkowe ustawienia oprócz funkcjonalności aplikacji. Może to spowodować, że Twoja aplikacja zostanie dostarczona później niż oczekiwano i będziesz mieć więcej kodu do utrzymania w przyszłości, a więcej kodu oznacza potencjalnie więcej błędów.
Zanim poświęcisz czas na optymalizację swojej aplikacji, zadaj sobie pytanie, czy aplikacja działa już płynnie, czy też ma jakąś niereagującą część, którą naprawdę należy zoptymalizować. Poświęcenie dużej ilości czasu na optymalizację już i tak płynnej aplikacji, aby skrócić o 0,01 sekundy, może nie być tego warte, ponieważ czas można lepiej poświęcić na opracowywanie lepszych funkcji lub innych priorytetów.
Dalsze zasoby
- „Apartament pysznych cukierków na iOS Eye”, Tim Oliver, Tokyo iOS Meetup 2018 (wideo)
- „Tworzenie współbieżnych interfejsów użytkownika w systemie iOS”, Andy Matuschak, WWDC 2012 (wideo)
- „Zachowywanie interfejsu użytkownika aplikacji podczas uruchamiania”, Apple
- „Przewodnik programowania współbieżności: kolejki wysyłki”, Archiwum dokumentacji, Apple
- „Sprawdzanie głównego wątku”, Apple