drawRect czy not drawRect (kiedy należy używać drawRect / Core Graphics a subviews / images i dlaczego?)

142

Aby wyjaśnić cel tego pytania: Wiem JAK tworzyć skomplikowane widoki zarówno z podwidokami, jak i przy użyciu drawRect. Staram się w pełni zrozumieć, kiedy i dlaczego należy używać jednego nad drugim.

Rozumiem też, że nie ma sensu optymalizować tak dużo wcześniej i robić coś trudniejszego przed wykonaniem jakiegokolwiek profilowania. Pomyśl, że czuję się komfortowo z obiema metodami i teraz naprawdę chcę głębszego zrozumienia.

Wiele mojego zamieszania wynika z uczenia się, jak sprawić, by przewijanie widoku tabeli było naprawdę płynne i szybkie. Oczywiście oryginalne źródło tej metody pochodzi od autora Twittera na iPhone'a (dawniej tweetie). Zasadniczo mówi się, że aby przewijanie tabeli było płynne, sekret polega na tym, aby NIE używać podglądów podrzędnych, ale zamiast tego robić wszystkie rysunki w jednym niestandardowym widoku. Zasadniczo wydaje się, że użycie wielu podglądów podrzędnych spowalnia renderowanie, ponieważ mają one dużo narzutów i są stale ponownie zestawiane z widokami nadrzędnymi.

Aby być uczciwym, zostało to napisane, gdy 3GS był całkiem nowy, a iDevices stały się znacznie szybsze od tego czasu. Mimo to ta metoda jest regularnie sugerowana w interwebach i innych miejscach w przypadku tabel o wysokiej wydajności. W rzeczywistości jest to sugerowana metoda w kodzie przykładowego kodu tabeli firmy Apple , została zasugerowana w kilku filmach WWDC ( praktyczne rysowanie dla programistów iOS ) i wielu książkach o programowaniu iOS .

Istnieją nawet niesamowicie wyglądające narzędzia do projektowania grafiki i generowania dla nich kodu Core Graphics.

Więc na początku sądzę, że „istnieje powód, dla którego istnieje Core Graphics. Jest SZYBKI!”

Ale gdy tylko myślę, że przychodzi mi do głowy pomysł „Korzystaj z grafiki rdzenia, kiedy to możliwe”, zaczynam dostrzegać, że drawRect jest często odpowiedzialny za słabą responsywność aplikacji, jest niezwykle kosztowny pod względem pamięci i naprawdę obciąża procesor. Zasadniczo powinienem „ unikać zastępowania drawRect ” (WWDC 2012 iOS App Performance: Graphics and Animations )

Więc myślę, że jak wszystko jest skomplikowane. Może pomożesz sobie i innym zrozumieć, kiedy i dlaczego warto używać drawRect?

Widzę kilka oczywistych sytuacji podczas korzystania z Core Graphics:

  1. Masz dane dynamiczne (przykład wykresu giełdowego Apple)
  2. Masz elastyczny element interfejsu użytkownika, którego nie można wykonać za pomocą prostego obrazu o zmiennym rozmiarze
  3. Tworzysz dynamiczną grafikę, która po wyrenderowaniu jest używana w wielu miejscach

Widzę sytuacje, w których należy unikać Core Graphics:

  1. Właściwości widoku muszą być animowane osobno
  2. Masz stosunkowo małą hierarchię widoków, więc każdy dostrzegalny dodatkowy wysiłek przy użyciu CG nie jest wart korzyści
  3. Chcesz zaktualizować fragmenty widoku bez przerysowywania całości
  4. Układ widoków podrzędnych należy aktualizować, gdy zmienia się rozmiar widoku nadrzędnego

Więc obdarz swoją wiedzą. W jakich sytuacjach sięgasz po drawRect / Core Graphics (można to również osiągnąć za pomocą podglądów podrzędnych)? Jakie czynniki doprowadziły Cię do takiej decyzji? Jak / dlaczego rysowanie w jednym niestandardowym widoku jest zalecane do płynnego przewijania komórek tabeli, ale Apple odradza drawRect ogólnie ze względu na wydajność? A co z prostymi obrazami tła (kiedy tworzysz je za pomocą grafiki komputerowej, a nie obrazu png o zmiennym rozmiarze)?

Dogłębne zrozumienie tego tematu może nie być potrzebne do tworzenia wartościowych aplikacji, ale nie lubię wybierać między technikami bez możliwości wyjaśnienia dlaczego. Mój mózg się na mnie wścieka.

Aktualizacja pytania

Dziękuję wszystkim za informację. Kilka pytań wyjaśniających:

  1. Jeśli rysujesz coś z podstawową grafiką, ale możesz osiągnąć to samo dzięki UIImageViews i wstępnie renderowanemu png, czy zawsze powinieneś iść tą drogą?
  2. Podobne pytanie: Zwłaszcza w przypadku narzędzi takich jak ten , kiedy należy rozważyć rysowanie elementów interfejsu w podstawowej grafice? (Prawdopodobnie gdy wyświetlanie twojego elementu jest zmienne. Np. Przycisk z 20 różnymi wariantami kolorystycznymi. Czy są jakieś inne przypadki?)
  3. Biorąc pod uwagę moje rozumienie w mojej odpowiedzi poniżej, czy ten sam wzrost wydajności dla komórki tabeli można uzyskać, efektywnie przechwytując bitmapę obrazu komórki po samym renderowaniu złożonego UIView i wyświetlając ją podczas przewijania i ukrywania złożonego widoku? Oczywiście niektóre elementy należałoby opracować. Miałem tylko interesującą myśl.
Bob Spryn
źródło

Odpowiedzi:

72

Trzymaj się UIKit i subviews, kiedy tylko możesz. Możesz być bardziej produktywny i korzystać ze wszystkich mechanizmów obiektowych, które powinny być łatwiejsze w utrzymaniu. Użyj grafiki podstawowej, gdy nie możesz uzyskać wymaganej wydajności z UIKit lub wiesz, że próba zhakowania efektów rysowania w UIKit byłaby bardziej skomplikowana.

Ogólny przepływ pracy powinien polegać na tworzeniu widoków tabeli z widokami podrzędnymi. Użyj Instrumentów, aby zmierzyć liczbę klatek na sekundę na najstarszym sprzęcie, który obsługuje Twoja aplikacja. Jeśli nie możesz uzyskać 60 FPS, przejdź do CoreGraphics. Kiedy robisz to przez jakiś czas, masz poczucie, że UIKit jest prawdopodobnie stratą czasu.

Dlaczego więc Core Graphics działa szybko?

CoreGraphics nie jest naprawdę szybki. Jeśli jest używany przez cały czas, prawdopodobnie jedziesz wolno. Jest to bogaty interfejs API do rysowania, który wymaga pracy na procesorze, w przeciwieństwie do dużej ilości pracy UIKit, która jest odciążana na GPU. Gdybyś musiał animować piłkę poruszającą się po ekranie, okropnym pomysłem byłoby wywołanie setNeedsDisplay na widoku 60 razy na sekundę. Tak więc, jeśli masz podkomponenty widoku, które wymagają osobnej animacji, każdy komponent powinien być osobną warstwą.

Innym problemem jest to, że jeśli nie wykonujesz niestandardowego rysowania za pomocą drawRect, UIKit może zoptymalizować widoki giełdowe, więc drawRect nie jest opcją lub może iść na skróty z kompozycją. Kiedy zastępujesz drawRect, UIKit musi obrać wolną ścieżkę, ponieważ nie ma pojęcia, co robisz.

Te dwa problemy można zrównoważyć korzyściami w przypadku komórek widoku tabeli. Po wywołaniu drawRect, gdy widok po raz pierwszy pojawia się na ekranie, zawartość jest buforowana, a przewijanie jest prostym tłumaczeniem wykonywanym przez GPU. Ponieważ masz do czynienia z pojedynczym widokiem, a nie złożoną hierarchią, optymalizacje drawRect w UIKit stają się mniej ważne. Więc wąskim gardłem staje się to, jak bardzo możesz zoptymalizować swój rysunek Core Graphics.

Kiedy tylko możesz, używaj UIKit. Wykonaj najprostszą implementację, która działa. Profil. Jeśli istnieje zachęta, zoptymalizuj.

Ben Sandofsky
źródło
53

Różnica polega na tym, że UIView i CALayer zasadniczo obsługują stałe obrazy. Te obrazy są przesyłane na kartę graficzną (jeśli znasz OpenGL, wyobraź sobie obraz jako teksturę, a UIView / CALayer jako wielokąt pokazujący taką teksturę). Gdy obraz znajduje się na GPU, można go narysować bardzo szybko, a nawet kilka razy, i (z niewielkim spadkiem wydajności) nawet z różnymi poziomami przezroczystości alfa na tle innych obrazów.

CoreGraphics / Quartz to API do generowania obrazów. Pobiera bufor pikseli (znowu myślę o teksturze OpenGL) i zmienia poszczególne piksele w nim. To wszystko dzieje się w pamięci RAM i na procesorze i dopiero po zakończeniu pracy Quartz obraz jest „przesyłany” z powrotem do procesora graficznego. Ta podróż w obie strony polegająca na uzyskaniu obrazu z GPU, zmianie go, a następnie przesłaniu całego obrazu (lub przynajmniej stosunkowo dużej jego części) z powrotem do GPU jest raczej powolna. Ponadto faktyczny rysunek, który robi Quartz, choć jest naprawdę szybki w tym, co robisz, jest znacznie wolniejszy niż to, co robi GPU.

To oczywiste, biorąc pod uwagę, że GPU porusza się głównie po niezmienionych pikselach w dużych fragmentach. Quartz ma losowy dostęp do pikseli i dzieli procesor z siecią, dźwiękiem itp. Ponadto, jeśli masz kilka elementów, które rysujesz za pomocą Quartz w tym samym czasie, musisz ponownie narysować je wszystkie po zmianie, a następnie przesłać cały fragment, a jeśli zmienisz jeden obraz, a następnie pozwolisz UIViews lub CALayers wkleić go do innych obrazów, możesz uciec z przesyłaniem znacznie mniejszych ilości danych do GPU.

Jeśli nie zaimplementujesz -drawRect:, większość widoków można po prostu zoptymalizować. Nie zawierają żadnych pikseli, więc nie mogą niczego narysować. Inne widoki, takie jak UIImageView, rysują tylko UIImage (co, znowu, zasadniczo jest odniesieniem do tekstury, która prawdopodobnie została już załadowana do GPU). Więc jeśli narysujesz tego samego UIImage 5 razy za pomocą UIImageView, zostanie on przesłany do GPU tylko raz, a następnie narysowany na wyświetlaczu w 5 różnych lokalizacjach, oszczędzając nam czas i procesor.

Implementacja -drawRect: powoduje utworzenie nowego obrazu. Następnie rysujesz to na procesorze za pomocą Quartz. Jeśli narysujesz UIImage w swoim drawRect, prawdopodobnie pobierze obraz z GPU, skopiuje go do obrazu, na który rysujesz, a kiedy skończysz, załaduje drugą kopię obrazu z powrotem na kartę graficzną. Więc używasz dwa razy więcej pamięci GPU w urządzeniu.

Dlatego najszybszym sposobem rysowania jest zwykle oddzielenie zawartości statycznej od zmieniającej się treści (w oddzielnych podklasach UIViews / UIView / CALayers). Załaduj zawartość statyczną jako UIImage i narysuj ją za pomocą UIImageView i umieść zawartość wygenerowaną dynamicznie w czasie wykonywania w drawRect. Jeśli masz zawartość, która jest wielokrotnie rysowana, ale sama z siebie nie zmienia się (tj. 3 ikony, które pojawiają się w tym samym slocie, aby wskazać jakiś status), użyj również UIImageView.

Jedno zastrzeżenie: istnieje coś takiego, jak zbyt wiele wyświetleń UIV. Szczególnie przezroczyste obszary mają większy wpływ na GPU podczas rysowania, ponieważ podczas wyświetlania muszą być mieszane z innymi pikselami za nimi. Dlatego możesz oznaczyć UIView jako „nieprzezroczysty”, aby wskazać GPU, że może on po prostu zniszczyć wszystko, co kryje się za tym obrazem.

Jeśli masz treść, która jest generowana dynamicznie w czasie wykonywania, ale pozostaje taka sama przez cały okres istnienia aplikacji (np. Etykieta zawierająca nazwę użytkownika), sensowne może być po prostu narysowanie całości raz za pomocą Quartz, z tekstem, obramowanie przycisku itp. jako część tła. Ale zwykle jest to optymalizacja, która nie jest potrzebna, chyba że aplikacja Instruments mówi inaczej.

bystrość
źródło
Dzięki za szczegółową odpowiedź. Miejmy nadzieję, że więcej osób przewinie w dół i zagłosuje również na to.
Bob Spryn,
Chciałbym móc dwukrotnie głosować za tym. Dzięki za wspaniały opis!
Sea Coast of Tibet
Lekka korekta: W większości przypadków nie ma w dół obciążenie z GPU, ale nadal jest w zasadzie przesyłania najwolniejsza operacja może poprosić GPU do zrobienia, ponieważ ma do przesyłania dużych buforów pikseli z wolniejszej pamięci systemowej RAM na szybsze VRAM. OTOH, gdy już jest obraz, przeniesienie go polega na wysłaniu nowego zestawu współrzędnych (kilku bajtów) do GPU, aby wiedzieć, gdzie narysować.
uliwitness
@uliwitness Przeglądałem twoją odpowiedź i wspomniałeś o oznaczeniu obiektu jako „nieprzezroczysty usuwa wszystko, co kryje się za tym obrazem”. Czy możesz rzucić trochę światła na tę część?
Rikh
@Rikh Oznaczenie warstwy jako nieprzezroczystej oznacza, że ​​a) w większości przypadków GPU nie będzie zawracać sobie głowy rysowaniem innych warstw pod tą warstwą, a przynajmniej ich części, które leżą pod warstwą nieprzezroczystą. Nawet jeśli zostały narysowane, w rzeczywistości nie wykona mieszania alfa, ale raczej skopiuje zawartość górnej warstwy na ekranie (zwykle w pełni przezroczyste piksele w nieprzezroczystej warstwie będą w końcu czarne lub przynajmniej nieprzezroczysta wersja ich koloru)
uliwitness
26

Zamierzam zachować podsumowanie tego, co ekstrapoluję na podstawie odpowiedzi innych osób, i zadać wyjaśniające pytania w aktualizacji pierwotnego pytania. Ale zachęcam innych do ciągłego przekazywania odpowiedzi i głosowania na tych, którzy dostarczyli dobrych informacji.

Ogólne podejście

Jest całkiem jasne, że ogólne podejście, jak wspomniał Ben Sandofsky w swojej odpowiedzi , powinno brzmieć: „Kiedy tylko możesz, używaj UIKit. Wykonaj najprostszą implementację, która działa. Profil. Kiedy pojawi się zachęta, zoptymalizuj”.

Dlaczego

  1. Istnieją dwa główne możliwe wąskie gardła w iDevice: procesor i procesor graficzny
  2. Procesor jest odpowiedzialny za wstępne rysowanie / renderowanie widoku
  3. GPU jest odpowiedzialny za większość animacji (animacja podstawowa), efekty warstw, komponowanie itp.
  4. UIView ma wiele wbudowanych optymalizacji, buforowania itp. Do obsługi złożonych hierarchii widoków
  5. Zastępując drawRect, tracisz wiele korzyści, które zapewnia UIView, i jest to generalnie wolniejsze niż pozwolenie UIView na obsługę renderowania.

Rysowanie zawartości komórek w jednym płaskim UIView może znacznie poprawić FPS na przewijanych tabelach.

Jak powiedziałem powyżej, CPU i GPU to dwa możliwe wąskie gardła. Ponieważ generalnie zajmują się różnymi rzeczami, musisz zwracać uwagę na to, z którym wąskim gardłem się napotykasz. W przypadku przewijanych tabel nie chodzi o to, że Core Graphics rysuje się szybciej i dlatego może znacznie poprawić FPS.

W rzeczywistości grafika podstawowa może być wolniejsza niż zagnieżdżona hierarchia UIView dla początkowego renderowania. Wydaje się jednak, że typowym powodem przerywanego przewijania jest to, że blokujesz GPU, więc musisz się tym zająć.

Dlaczego zastąpienie drawRect (przy użyciu podstawowej grafiki) może pomóc w przewijaniu tabeli:

Z tego, co rozumiem, procesor graficzny nie jest odpowiedzialny za początkowe renderowanie widoków, ale zamiast tego przekazuje tekstury lub mapy bitowe, czasami z pewnymi właściwościami warstw, po ich wyrenderowaniu. Następnie jest odpowiedzialny za kompozycję bitmap, renderowanie wszystkich tych warstw i większości animacji (Core Animation).

W przypadku komórek widoku tabeli GPU może mieć wąskie gardło ze względu na złożone hierarchie widoków, ponieważ zamiast animować jedną bitmapę, animuje widok nadrzędny i wykonuje obliczenia układu widoku podrzędnego, renderuje efekty warstw i komponuje wszystkie podwidoki. Więc zamiast animować jedną bitmapę, jest odpowiedzialny za związek kilku bitmap i ich interakcję dla tego samego obszaru pikseli.

Podsumowując, powodem, dla którego rysowanie komórki w jednym widoku z podstawową grafiką może przyspieszyć przewijanie tabeli, NIE jest to, że rysuje ona szybciej, ale dlatego, że zmniejsza obciążenie GPU, które jest wąskim gardłem powodującym problemy w tym konkretnym scenariuszu .

Bob Spryn
źródło
1
Ostrożnie. Układy GPU efektywnie ponownie składają całe drzewo warstw dla każdej klatki. Zatem przenoszenie warstwy jest praktycznie zerowe. Jeśli utworzysz jedną dużą warstwę, tylko przeniesienie jednej warstwy jest szybsze niż przeniesienie kilku. Przeniesienie czegoś w tej warstwie nagle wymaga użycia procesora i ma koszt. Ponieważ zawartość komórek zwykle nie zmienia się zbytnio (tylko w odpowiedzi na stosunkowo rzadkie działania użytkownika), użycie mniejszej liczby widoków przyspiesza działanie. Ale zrobienie jednego dużego widoku ze wszystkimi komórkami byłoby złym pomysłem ™.
uliwitness
6

Jestem twórcą gier i zadawałem te same pytania, gdy mój przyjaciel powiedział mi, że moja hierarchia widoków oparta na UIImageView spowolni moją grę i sprawi, że będzie straszna. Następnie zacząłem badać wszystko, co mogłem znaleźć, o tym, czy używać UIViews, CoreGraphics, OpenGL lub czegoś innego, jak Cocos2D. Konsekwentna odpowiedź, jaką otrzymałem od przyjaciół, nauczycieli i inżynierów Apple w WWDC, brzmiała, że ​​ostatecznie nie będzie dużej różnicy, ponieważ na pewnym poziomie wszyscy robią to samo . Opcje wyższego poziomu, takie jak UIViews, opierają się na opcjach niższego poziomu, takich jak CoreGraphics i OpenGL, po prostu są opakowane w kod, aby ułatwić korzystanie z nich.

Nie używaj CoreGraphics, jeśli zamierzasz tylko ponownie napisać UIView. Możesz jednak przyspieszyć korzystanie z CoreGraphics, o ile wykonujesz wszystkie rysunki w jednym widoku, ale czy naprawdę warto ? Odpowiedź, którą znalazłem, zwykle brzmi: nie. Kiedy zaczynałem grę, pracowałem z iPhonem 3G. Gdy moja gra stawała się coraz bardziej złożona, zacząłem dostrzegać pewne opóźnienia, ale na nowszych urządzeniach było to całkowicie niezauważalne. Teraz dzieje się dużo akcji, a jedynym opóźnieniem wydaje się być spadek o 1-3 fps podczas grania na najbardziej złożonym poziomie na iPhonie 4.

Mimo to zdecydowałem się użyć Instruments, aby znaleźć funkcje, które zajmowały najwięcej czasu. Okazało się, że problemy nie były związane z korzystaniem z UIViews . Zamiast tego wielokrotnie wywoływał CGRectMake w celu wykonania pewnych obliczeń wykrywania kolizji i ładowania plików obrazu i dźwięku oddzielnie dla niektórych klas, które używają tych samych obrazów, zamiast pobierać je z jednej centralnej klasy pamięci.

Więc ostatecznie możesz być w stanie osiągnąć niewielki zysk z używania CoreGraphics, ale zwykle nie będzie to tego warte lub może nie mieć żadnego efektu. Jedyny raz, kiedy używam CoreGraphics, to rysowanie kształtów geometrycznych, a nie tekstu i obrazów.

WolfLink
źródło
8
Myślę, że możesz mylić Core Animation z Core Graphics. Grafika podstawowa jest naprawdę powolnym rysunkiem opartym na oprogramowaniu i nie jest używana do rysowania zwykłych widoków UIV, chyba że zastąpisz metodę drawRect. Core Animation jest przyspieszany sprzętowo, szybki i odpowiedzialny za większość tego, co widzisz na ekranie.
Nick Lockwood,