zu drawRect oder nicht zu drawRect (wann sollte man drawRect / Core Graphics gegen Unteransichten / Bilder verwenden und warum?)

142

Um den Zweck dieser Frage zu klären: Ich weiß, wie man komplizierte Ansichten mit beiden Unteransichten und mit drawRect erstellt. Ich versuche zu verstehen, wann und warum man übereinander verwendet.

Ich verstehe auch, dass es keinen Sinn macht, so viel im Voraus zu optimieren und etwas schwieriger zu machen, bevor Sie ein Profiling durchführen. Bedenken Sie, dass ich mit beiden Methoden vertraut bin und jetzt wirklich ein tieferes Verständnis möchte.

Ein großer Teil meiner Verwirrung ist darauf zurückzuführen, dass ich gelernt habe, wie man die Bildlaufleistung in der Tabellenansicht wirklich reibungslos und schnell macht. Natürlich stammt die ursprüngliche Quelle dieser Methode vom Autor hinter Twitter für iPhone (ehemals Tweetie). Grundsätzlich heißt es, dass das Geheimnis darin besteht, KEINE Unteransichten zu verwenden, um das Scrollen von Tabellen reibungslos zu gestalten, sondern die gesamte Zeichnung in einer benutzerdefinierten Ansicht zu erstellen. Im Wesentlichen scheint es, dass die Verwendung vieler Unteransichten das Rendern verlangsamt, da sie viel Overhead haben und ständig über ihren übergeordneten Ansichten neu zusammengesetzt werden.

Um fair zu sein, wurde dies geschrieben, als das 3GS ziemlich brandneu war und iDevices seitdem viel schneller geworden sind. Noch diese Methode wird regelmäßig vorgeschlagen auf den interwebs und anderswo für Hochleistungstabellen. Tatsächlich ist es eine empfohlene Methode in Apples Tabellenbeispielcode , die in mehreren WWDC-Videos ( Praktisches Zeichnen für iOS-Entwickler ) und vielen iOS- Programmierbüchern vorgeschlagen wurde .

Es gibt sogar fantastisch aussehende Tools , um Grafiken zu entwerfen und Core Graphics-Code für sie zu generieren.

Zuerst muss ich glauben: "Es gibt einen Grund, warum Core Graphics existiert. Es ist SCHNELL!"

Sobald ich jedoch auf die Idee komme, "Core Graphics nach Möglichkeit zu bevorzugen", stelle ich fest, dass drawRect häufig für eine schlechte Reaktionsfähigkeit in einer App verantwortlich ist, extrem teuer im Speicher ist und die CPU wirklich belastet. Grundsätzlich sollte ich vermeiden, drawRect zu überschreiben (WWDC 2012 iOS App Performance: Grafiken und Animationen )

Ich denke, wie alles ist es kompliziert. Vielleicht können Sie mir und anderen helfen, das Wann und Warum für die Verwendung von drawRect zu verstehen?

Ich sehe einige offensichtliche Situationen bei der Verwendung von Core Graphics:

  1. Sie haben dynamische Daten (Apple Stock Chart Beispiel)
  2. Sie haben ein flexibles UI-Element, das nicht mit einem einfachen Bild mit veränderbarer Größe ausgeführt werden kann
  3. Sie erstellen eine dynamische Grafik, die nach dem Rendern an mehreren Stellen verwendet wird

Ich sehe Situationen, um Core Graphics zu vermeiden:

  1. Die Eigenschaften Ihrer Ansicht müssen separat animiert werden
  2. Sie haben eine relativ kleine Ansichtshierarchie, sodass sich ein wahrgenommener zusätzlicher Aufwand bei der Verwendung von CG nicht lohnt
  3. Sie möchten Teile der Ansicht aktualisieren, ohne das Ganze neu zu zeichnen
  4. Das Layout Ihrer Unteransichten muss aktualisiert werden, wenn sich die Größe der übergeordneten Ansicht ändert

Geben Sie also Ihr Wissen. In welchen Situationen greifen Sie nach drawRect / Core Graphics (dies könnte auch mit Unteransichten erreicht werden)? Welche Faktoren führen Sie zu dieser Entscheidung? Wie / Warum wird das Zeichnen in einer benutzerdefinierten Ansicht für das Scrollen mit butterweichen Tabellenzellen empfohlen, Apple rät jedoch aus Leistungsgründen generell von drawRect ab? Was ist mit einfachen Hintergrundbildern (wann erstellen Sie sie mit CG oder mit einem anpassbaren PNG-Bild)?

Ein tiefes Verständnis dieses Themas ist möglicherweise nicht erforderlich, um lohnende Apps zu erstellen, aber ich mag es nicht, zwischen Techniken zu wählen, ohne erklären zu können, warum. Mein Gehirn wird sauer auf mich.

Frage Update

Vielen Dank für die Informationen an alle. Einige klärende Fragen hier:

  1. Wenn Sie etwas mit Kerngrafiken zeichnen, aber mit UIImageViews und einem vorgerenderten PNG dasselbe erreichen können, sollten Sie immer diesen Weg gehen?
  2. Eine ähnliche Frage: Wann sollten Sie vor allem bei Badass-Tools wie diesen in Betracht ziehen, Oberflächenelemente in Kerngrafiken zu zeichnen? (Wahrscheinlich, wenn die Anzeige Ihres Elements variabel ist. ZB eine Schaltfläche mit 20 verschiedenen Farbvarianten. Andere Fälle?)
  3. Könnten nach meinem Verständnis in meiner Antwort unten möglicherweise die gleichen Leistungssteigerungen für eine Tabellenzelle erzielt werden, indem eine Snapshot-Bitmap Ihrer Zelle nach dem Rendern Ihres komplexen UIView-Renderings effektiv erfasst und beim Scrollen und Ausblenden Ihrer komplexen Ansicht angezeigt wird? Offensichtlich müssten einige Stücke ausgearbeitet werden. Nur ein interessanter Gedanke, den ich hatte.
Bob Spryn
quelle

Antworten:

72

Halten Sie sich an UIKit und Unteransichten, wann immer Sie können. Sie können produktiver sein und alle OO-Mechanismen nutzen, die einfacher zu warten sind. Verwenden Sie Core Graphics, wenn Sie mit UIKit nicht die Leistung erzielen können, die Sie benötigen, oder wenn Sie wissen, dass der Versuch, Zeichnungseffekte in UIKit zu hacken, komplizierter wäre.

Der allgemeine Workflow sollte darin bestehen, die Tabellenansichten mit Unteransichten zu erstellen. Verwenden Sie Instrumente, um die Bildrate auf der ältesten Hardware zu messen, die Ihre App unterstützt. Wenn Sie keine 60 fps erhalten können, gehen Sie zu CoreGraphics. Wenn Sie dies für eine Weile getan haben, bekommen Sie ein Gefühl dafür, wann UIKit wahrscheinlich Zeitverschwendung ist.

Warum ist Core Graphics also schnell?

CoreGraphics ist nicht sehr schnell. Wenn es die ganze Zeit verwendet wird, werden Sie wahrscheinlich langsam. Es handelt sich um eine umfangreiche Zeichnungs-API, für die die Arbeit auf der CPU ausgeführt werden muss, im Gegensatz zu vielen UIKit-Arbeiten, die auf die GPU ausgelagert werden. Wenn Sie einen Ball animieren müssten, der sich über den Bildschirm bewegt, wäre es eine schreckliche Idee, setNeedsDisplay 60 Mal pro Sekunde für eine Ansicht aufzurufen. Wenn Sie also Unterkomponenten Ihrer Ansicht haben, die einzeln animiert werden müssen, sollte jede Komponente eine separate Ebene sein.

Das andere Problem ist, dass UIKit, wenn Sie mit drawRect kein benutzerdefiniertes Zeichnen ausführen, die Bestandsansichten optimieren kann, sodass drawRect ein No-Op ist, oder Verknüpfungen mit Compositing verwenden kann. Wenn Sie drawRect überschreiben, muss UIKit den langsamen Pfad einschlagen, da es keine Ahnung hat, was Sie tun.

Diese beiden Probleme können durch Vorteile bei Tabellenansichtszellen aufgewogen werden. Nachdem drawRect aufgerufen wird, wenn eine Ansicht zum ersten Mal auf dem Bildschirm angezeigt wird, wird der Inhalt zwischengespeichert, und das Scrollen ist eine einfache Übersetzung, die von der GPU ausgeführt wird. Da es sich nicht um eine komplexe Hierarchie, sondern um eine einzelne Ansicht handelt, verlieren die drawRect-Optimierungen von UIKit an Bedeutung. Der Engpass besteht also darin, wie stark Sie Ihre Core Graphics-Zeichnung optimieren können.

Verwenden Sie UIKit, wann immer Sie können. Führen Sie die einfachste Implementierung aus, die funktioniert. Profil. Wenn es einen Anreiz gibt, optimieren Sie.

Ben Sandofsky
quelle
53

Der Unterschied besteht darin, dass sich UIView und CALayer im Wesentlichen mit festen Bildern befassen. Diese Bilder werden auf die Grafikkarte hochgeladen (wenn Sie OpenGL kennen, stellen Sie sich ein Bild als Textur und einen UIView / CALayer als Polygon mit einer solchen Textur vor). Sobald sich ein Bild auf der GPU befindet, kann es sehr schnell und sogar mehrmals gezeichnet werden und (mit einem leichten Leistungsverlust) auch bei unterschiedlichen Alpha-Transparenzstufen über anderen Bildern.

CoreGraphics / Quartz ist eine API zum Generieren von Bildern. Es benötigt einen Pixelpuffer (denken Sie wieder an OpenGL-Textur) und ändert einzelne Pixel darin. Dies alles geschieht im RAM und auf der CPU, und erst wenn Quartz fertig ist, wird das Image zurück auf die GPU "gespült". Dieser Roundtrip, bei dem ein Image von der GPU abgerufen, geändert und dann das gesamte Image (oder zumindest ein vergleichsweise großer Teil davon) wieder auf die GPU hochgeladen wird, ist ziemlich langsam. Außerdem ist die eigentliche Zeichnung, die Quartz erstellt, zwar sehr schnell für das, was Sie tun, aber viel langsamer als die GPU.

Das ist offensichtlich, wenn man bedenkt, dass sich die GPU hauptsächlich in großen Blöcken um unveränderte Pixel bewegt. Quartz greift nach dem Zufallsprinzip auf Pixel zu und teilt die CPU mit Netzwerk, Audio usw. Wenn Sie mehrere Elemente gleichzeitig mit Quartz zeichnen, müssen Sie alle Elemente neu zeichnen, wenn sich eines ändert, und dann das hochladen Wenn Sie ein Bild ändern und UIViews oder CALayers es dann in Ihre anderen Bilder einfügen lassen, können Sie viel kleinere Datenmengen auf die GPU hochladen.

Wenn Sie -drawRect: nicht implementieren, können die meisten Ansichten einfach entfernt werden. Sie enthalten keine Pixel und können daher nichts zeichnen. Andere Ansichten, wie UIImageView, zeichnen nur ein UIImage (was wiederum im Wesentlichen eine Referenz auf eine Textur ist, die wahrscheinlich bereits auf die GPU geladen wurde). Wenn Sie also dasselbe UIImage fünfmal mit einer UIImageView zeichnen, wird es nur einmal auf die GPU hochgeladen und dann an fünf verschiedenen Orten auf das Display gezeichnet, was uns Zeit und CPU spart.

Wenn Sie -drawRect: implementieren, wird ein neues Image erstellt. Sie zeichnen dann mit Quarz auf die CPU. Wenn Sie ein UIImage in Ihrem drawRect zeichnen, wird das Bild wahrscheinlich von der GPU heruntergeladen, in das Bild kopiert, auf das Sie zeichnen, und wenn Sie fertig sind, wird diese zweite Kopie des Bildes wieder auf die Grafikkarte hochgeladen. Sie verwenden also den doppelten GPU-Speicher des Geräts.

Der schnellste Weg zum Zeichnen besteht normalerweise darin, statische Inhalte von sich ändernden Inhalten zu trennen (in separaten UIViews / UIView-Unterklassen / CALayers). Laden Sie statischen Inhalt als UIImage und zeichnen Sie ihn mit einer UIImageView. Fügen Sie zur Laufzeit dynamisch generierten Inhalt in ein drawRect ein. Wenn Sie Inhalte haben, die wiederholt gezeichnet werden, sich aber nicht ändern (dh 3 Symbole, die im selben Slot angezeigt werden, um einen bestimmten Status anzuzeigen), verwenden Sie auch UIImageView.

Eine Einschränkung: Es gibt so etwas wie zu viele UIViews. Besonders transparente Bereiche fordern einen höheren Tribut von der GPU zum Zeichnen, da sie bei der Anzeige mit anderen Pixeln dahinter gemischt werden müssen. Aus diesem Grund können Sie eine UIView als "undurchsichtig" markieren, um der GPU anzuzeigen, dass sie einfach alles hinter diesem Bild auslöschen kann.

Wenn Sie Inhalte haben, die zur Laufzeit dynamisch generiert werden, aber für die Dauer der Lebensdauer der Anwendung gleich bleiben (z. B. eine Beschriftung mit dem Benutzernamen), kann es tatsächlich sinnvoll sein, das Ganze nur einmal mit Quartz zu zeichnen, mit dem Text, dem Schaltflächenrand usw. als Teil des Hintergrunds. Aber das ist normalerweise eine Optimierung, die nicht benötigt wird, es sei denn, die Instruments-App sagt es Ihnen anders.

uliwitness
quelle
Danke für die ausführliche Antwort. Hoffentlich scrollen mehr Leute nach unten und stimmen dies ebenfalls ab.
Bob Spryn
Ich wünschte, ich könnte dies zweimal positiv bewerten. Vielen Dank für die tolle Berichterstattung!
Seeküste von Tibet
Leichte Korrektur: In den meisten Fällen gibt es keine nach unten Last von den GPU, aber der Upload ist im Grunde noch die langsamste Operation Sie eine GPU zu tun fragen, weil es große Pixelpuffer aus langsamem System - RAM in schnellen VRAM zu übertragen hat. OTOH Sobald ein Bild vorhanden ist, muss beim Verschieben lediglich ein neuer Satz von Koordinaten (einige Bytes) an die GPU gesendet werden, damit ich weiß, wo ich zeichnen muss.
uliwitness
@uliwitness Ich habe Ihre Antwort durchgesehen und Sie haben erwähnt, dass ein Objekt als "undurchsichtig löscht alles hinter diesem Bild" markiert ist. Können Sie bitte etwas Licht in diesen Teil bringen?
Rikh
@Rikh Das Markieren einer Ebene als undurchsichtig bedeutet, dass a) in den meisten Fällen die GPU nicht die Mühe macht, andere Ebenen unter dieser Ebene zu zeichnen, zumindest die Teile davon, die unter der undurchsichtigen Ebene liegen. Selbst wenn sie gezeichnet wurden, wird keine Alpha-Überblendung durchgeführt, sondern lediglich der Inhalt der obersten Ebene auf den Bildschirm kopiert (normalerweise werden vollständig transparente Pixel in einer undurchsichtigen Ebene schwarz oder zumindest eine undurchsichtige Version ihrer Farbe angezeigt).
uliwitness
26

Ich werde versuchen, eine Zusammenfassung dessen zu erstellen, was ich aus den Antworten anderer hier extrapoliere, und in einem Update der ursprünglichen Frage klärende Fragen stellen. Aber ich ermutige andere, immer wieder Antworten zu geben und diejenigen abzustimmen, die gute Informationen geliefert haben.

Allgemeiner Ansatz

Es ist ziemlich klar, dass der allgemeine Ansatz, wie Ben Sandofsky in seiner Antwort erwähnte , lauten sollte: "Wann immer Sie können, verwenden Sie UIKit. Führen Sie die einfachste Implementierung durch, die funktioniert. Profil. Wenn es einen Anreiz gibt, optimieren Sie."

Das Warum

  1. Es gibt zwei mögliche Hauptengpässe in einem iDevice, die CPU und die GPU
  2. Die CPU ist für das anfängliche Zeichnen / Rendern einer Ansicht verantwortlich
  3. Die GPU ist für einen Großteil der Animation (Kernanimation), Ebeneneffekte, Compositing usw. verantwortlich.
  4. UIView verfügt über zahlreiche Optimierungen, Caching usw. für die Verarbeitung komplexer Ansichtshierarchien
  5. Wenn Sie drawRect überschreiben, verpassen Sie viele der Vorteile, die UIView bietet, und es ist im Allgemeinen langsamer, als wenn UIView das Rendern übernimmt.

Durch das Zeichnen von Zelleninhalten in einer flachen UIView können Sie Ihre FPS beim Scrollen von Tabellen erheblich verbessern.

Wie ich oben sagte, sind CPU und GPU zwei mögliche Engpässe. Da sie im Allgemeinen unterschiedliche Dinge behandeln, müssen Sie darauf achten, auf welchen Engpass Sie stoßen.Beim Scrollen von Tabellen ist es nicht so, dass Core Graphics schneller zeichnet, und deshalb kann es Ihre FPS erheblich verbessern.

Tatsächlich ist Core Graphics möglicherweise sehr langsam langsamer als eine verschachtelte UIView-Hierarchie für das anfängliche Rendern. Es scheint jedoch, dass der typische Grund für abgehacktes Scrollen darin besteht, dass Sie einen Engpass bei der GPU haben. Daher müssen Sie dies beheben.

Warum das Überschreiben von drawRect (unter Verwendung von Kerngrafiken) das Scrollen von Tabellen erleichtern kann:

Soweit ich weiß, ist die GPU nicht für das anfängliche Rendern der Ansichten verantwortlich, sondern erhält stattdessen Texturen oder Bitmaps, manchmal mit einigen Ebeneneigenschaften, nachdem sie gerendert wurden. Es ist dann für das Zusammensetzen der Bitmaps, das Rendern all dieser Ebeneneffekte und den Großteil der Animation (Kernanimation) verantwortlich.

Bei Zellen mit Tabellenansicht kann die GPU mit komplexen Ansichtshierarchien einen Engpass aufweisen, da anstelle der Animation einer Bitmap die übergeordnete Ansicht animiert und Berechnungen des Unteransichtslayouts durchgeführt, Ebeneneffekte gerendert und alle Unteransichten zusammengesetzt werden. Anstatt eine Bitmap zu animieren, ist sie für die Beziehung zwischen mehreren Bitmaps und deren Interaktion für denselben Pixelbereich verantwortlich.

Zusammenfassend lässt sich sagen, dass der Grund, warum das Zeichnen Ihrer Zelle in einer Ansicht mit Kerngrafiken das Scrollen in der Tabelle beschleunigen kann, NICHT darin besteht, dass das Zeichnen schneller erfolgt, sondern dass die GPU entlastet wird. Dies ist der Engpass, der Ihnen in diesem bestimmten Szenario Probleme bereitet .

Bob Spryn
quelle
1
Aber vorsichtig. GPUs setzen den gesamten Ebenenbaum für jeden Frame effektiv neu zusammen. Das Verschieben einer Ebene ist also praktisch ohne Kosten. Wenn Sie eine große Ebene erstellen, ist das Verschieben dieser einen Ebene schneller als das Verschieben mehrerer Ebenen. Das Verschieben von Objekten innerhalb dieser Ebene ist plötzlich mit der CPU verbunden und mit Kosten verbunden. Da sich der Zelleninhalt normalerweise nicht wesentlich ändert (nur als Reaktion auf vergleichsweise seltene Benutzeraktionen), beschleunigt die Verwendung weniger Ansichten die Arbeit. Eine große Ansicht mit allen Zellen zu erstellen, wäre jedoch eine schlechte Idee.
uliwitness
6

Ich bin ein Spieleentwickler und habe die gleichen Fragen gestellt, als mein Freund mir sagte, dass meine UIImageView-basierte Ansichtshierarchie mein Spiel verlangsamen und es schrecklich machen würde. Anschließend recherchierte ich alles, was ich über die Verwendung von UIViews, CoreGraphics, OpenGL oder einem Drittanbieter wie Cocos2D finden konnte. Die konsequente Antwort, die ich von Freunden, Lehrern und Apple-Ingenieuren bei WWDC erhielt, war, dass es am Ende keinen großen Unterschied geben wird, weil sie auf einer bestimmten Ebene alle dasselbe tun . Optionen auf höherer Ebene wie UIViews basieren auf Optionen auf niedrigerer Ebene wie CoreGraphics und OpenGL. Sie sind lediglich in Code eingeschlossen, um Ihnen die Verwendung zu erleichtern.

Verwenden Sie CoreGraphics nicht, wenn Sie die UIView nur neu schreiben möchten. Durch die Verwendung von CoreGraphics können Sie jedoch etwas an Geschwindigkeit gewinnen, solange Sie alle Zeichnungen in einer Ansicht ausführen. Dies lohnt sich jedoch wirklich es sich ? Die Antwort, die ich gefunden habe, ist normalerweise nein. Als ich mein Spiel zum ersten Mal startete, arbeitete ich mit dem iPhone 3G. Als mein Spiel immer komplexer wurde, bemerkte ich einige Verzögerungen, aber bei den neueren Geräten war dies völlig unbemerkt. Jetzt ist viel los und die einzige Verzögerung scheint ein Rückgang von 1-3 fps zu sein, wenn ich auf einem iPhone 4 im komplexesten Level spiele.

Trotzdem entschied ich mich, Instrumente zu verwenden, um die Funktionen zu finden, die am meisten Zeit in Anspruch nahmen. Ich stellte fest, dass die Probleme nicht mit meiner Verwendung von UIViews zusammenhängen . Stattdessen wurde CGRectMake wiederholt für bestimmte Kollisionserkennungsberechnungen aufgerufen und Bild- und Audiodateien für bestimmte Klassen, die dieselben Bilder verwenden, separat geladen, anstatt sie aus einer zentralen Speicherklasse ziehen zu lassen.

Am Ende können Sie vielleicht einen leichten Gewinn mit CoreGraphics erzielen, aber normalerweise lohnt es sich nicht oder hat überhaupt keine Wirkung. Ich verwende CoreGraphics nur, wenn ich geometrische Formen anstelle von Text und Bildern zeichne.

WolfLink
quelle
8
Ich denke, Sie verwechseln möglicherweise Core Animation mit Core Graphics. Core Graphics ist ein sehr langsames, softwarebasiertes Zeichnen und wird nicht zum Zeichnen regulärer UIViews verwendet, es sei denn, Sie überschreiben die drawRect-Methode. Core Animation ist hardwarebeschleunigt, schnell und verantwortlich für das meiste, was Sie auf dem Bildschirm sehen.
Nick Lockwood