Wie kann ich Linien schneller als CGContextStrokePath rendern?

8

Ich zeichne ~ 768 Punkte für ein Diagramm mit CGContextStrokePath. Das Problem ist, dass ich jede Sekunde einen neuen Datenpunkt bekomme und so den Graph neu zeichne. Dies nimmt derzeit 50% CPU in was ist schon eine ausgelastete App.

Das Zeichnen von Grafiken erfolgt in drawRect in einer UIView. Der Graph ist zeitbasiert, so dass neue Datenpunkte immer auf der rechten Seite ankommen.

Ich denke ein paar alternative Ansätze:

  1. Zeichnen Sie mit GLKit (kostet nicht ältere Geräte) und es sieht nach viel Arbeit aus.
  2. Machen Sie eine Art Screen-Grab (renderInContext?), verschieben Sie sie um 1 px nach links, blit und zeichnen Sie nur eine Linie für die letzten beiden Datenpunkte.
  3. Haben Sie einen sehr breiten CALayer und schwenken Sie ihn entlang?
  4. Glätten Sie den Datensatz, aber das fühlt sich an wie Betrug:)

Es ist auch möglich, dass mir etwas offensichtlich fehlt, dass ich so schlechte Leistung sehe?

%Vor%     
Peter 28.08.2012, 15:43
quelle

3 Antworten

14

Lassen Sie uns eine grafische Ansicht implementieren, die eine Reihe von großen, dünnen Ebenen verwendet, um die Anzahl der benötigten Neuzeichnungen zu reduzieren. Wir schieben die Ebenen beim Hinzufügen von Samples nach links, sodass zu jeder Zeit wahrscheinlich eine Ebene über der linken Seite der Ansicht und eine über der rechten Seite der Ansicht hängen:

Auf meinem github-Konto finden Sie ein vollständiges funktionierendes Beispiel für den folgenden Code.

Konstanten

Wir machen jede Ebene 32 Punkte breit:

%Vor%

Nehmen wir an, wir werden die Samples entlang der X-Achse mit einem Sample pro Punkt teilen:

%Vor%

So können wir die Anzahl der Proben pro Schicht ableiten. Lassen Sie uns eine Ebene als Beispiele für eine Kachel bezeichnen:

%Vor%

Wenn wir eine Ebene zeichnen, können wir die Muster nicht einfach innerhalb der Ebene zeichnen. Wir müssen ein oder zwei Muster nach jeder Kante zeichnen, weil die Linien zu diesen Mustern den Rand der Ebene kreuzen. Wir nennen dies die Padding-Beispiele :

%Vor%

Die maximale Größe eines iPhone-Bildschirms beträgt 320 Punkte. Daher können wir die maximale Anzahl von Proben berechnen, die wir beibehalten müssen:

%Vor%

(Sie sollten die 320 ändern, wenn Sie auf einem iPad laufen möchten.)

Wir müssen in der Lage sein zu berechnen, welche Kachel ein bestimmtes Sample enthält. Und wie Sie sehen werden, möchten wir dies auch dann tun, wenn die Probennummer negativ ist, weil spätere Berechnungen dadurch einfacher werden:

%Vor%

Instanzvariablen

Um nun GraphView zu implementieren, benötigen wir einige Instanzvariablen. Wir müssen die Layer speichern, die wir zum Zeichnen des Diagramms verwenden. Und wir wollen in der Lage sein, jede Ebene nach ihrer graphischen Darstellung zu suchen:

%Vor%

In einem realen Projekt möchten Sie die Beispiele in einem Modellobjekt speichern und der Ansicht einen Verweis auf das Modell geben. Aber für dieses Beispiel speichern wir die Beispiele einfach in der Ansicht:

%Vor%

Da wir keine beliebig große Anzahl von Samples speichern wollen, werden wir alte Samples verwerfen, wenn _samples groß wird. Aber es wird die Implementierung vereinfachen, wenn wir meistens so tun können, dass wir keine Proben verwerfen. Um dies zu tun, verfolgen wir die Gesamtzahl der jemals erhaltenen Samples.

%Vor%

Wir sollten vermeiden, den Hauptthread zu blockieren, also werden wir in einer separaten GCD-Warteschlange zeichnen. Wir müssen verfolgen, welche Kacheln in dieser Warteschlange gezeichnet werden müssen. Um zu vermeiden, dass eine ausstehende Kachel mehr als einmal gezeichnet wird, verwenden wir anstelle eines Arrays eine Menge (die Duplikate eliminiert):

%Vor%

Und hier ist die GCD-Warteschlange, auf der wir die Zeichnung machen werden.

%Vor%

Initialisierung / Zerstörung

Damit diese Ansicht funktioniert, egal ob Sie sie in Code oder in einer Nib erstellen, benötigen wir zwei Initialisierungsmethoden:

%Vor%

Beide Methoden rufen commonInit auf, um die eigentliche Initialisierung durchzuführen:

%Vor%

ARC wird die GCD-Warteschlange für uns nicht bereinigen:

%Vor%

Hinzufügen eines Beispiels

Um ein neues Beispiel hinzuzufügen, wählen wir eine Zufallszahl und hängen sie an _samples an. Wir erhöhen auch _totalSampleCount . Wir verwerfen die ältesten Beispiele, wenn _samples groß geworden ist.

%Vor%

Dann prüfen wir, ob wir eine neue Kachel gestartet haben. Wenn dies der Fall ist, finden wir die Ebene, die die älteste Kachel gezeichnet hat, und verwenden sie erneut, um die neu erstellte Kachel zu zeichnen.

%Vor%

Jetzt berechnen wir das Layout aller Ebenen, die sich etwas nach links bewegen, damit das neue Muster im Diagramm sichtbar wird.

%Vor%

Zum Schluss fügen wir Kacheln zur Redraw-Warteschlange hinzu.

%Vor%

Wir wollen keine Proben einzeln verwerfen. Das wäre ineffizient. Stattdessen lassen wir den Müll für eine Weile aufgehen und werfen ihn dann auf einmal weg:

%Vor%

Um eine Ebene für die neue Kachel wiederzuverwenden, müssen wir die Ebene der ältesten Kachel finden:

%Vor%

Nun können wir es unter dem alten Schlüssel aus dem _tileLayers -Wörterbuch entfernen und unter dem neuen Schlüssel speichern:

%Vor%

Wenn wir den wiederverwendeten Layer an seine neue Position verschieben, animiert Core Animation standardmäßig das Verschieben des Layers. Wir wollen das nicht, weil es ein großes leeres oranges Rechteck sein wird, das über unser Diagramm gleitet. Wir möchten es sofort bewegen:

%Vor%

Wenn wir ein Beispiel hinzufügen, wollen wir immer die Kachel mit dem Muster neu zeichnen. Wir müssen auch die vorherige Kachel neu zeichnen, wenn sich die neue Probe innerhalb des Auffüllbereichs der vorherigen Kachel befindet.

%Vor%

Wenn Sie eine Kachel zum Neuzeichnen in die Warteschlange stellen, müssen Sie sie nur zum Redraw-Set hinzufügen und einen Block senden, um sie auf _redrawQueue neu zu zeichnen.

%Vor%

Layout

Das System sendet layoutSubviews an die GraphView , wenn es zum ersten Mal erscheint, und jedes Mal, wenn sich die Größe ändert (z. B. wenn eine Gerätedrehung die Größe ändert). Und wir erhalten nur die layoutSubviews -Nachricht, wenn wir wirklich auf dem Bildschirm erscheinen, mit unseren endgültigen Grenzen. Daher ist layoutSubviews ein guter Ort, um die Kachelebenen einzurichten.

Zuerst müssen wir Schichten erstellen oder entfernen, damit wir die richtigen Schichten für unsere Größe haben.Dann müssen wir die Schichten auslegen, indem wir ihre Rahmen entsprechend einstellen. Schließlich müssen wir für jede Ebene ihre Kachel zur Neuzeichnung einreihen.

%Vor%

Wenn Sie das Kachelverzeichnis anpassen, müssen Sie für jede sichtbare Kachel eine Ebene einrichten und Ebenen für nicht sichtbare Kacheln entfernen. Wir werden das Wörterbuch jedes Mal von Grund auf neu erstellen, aber wir werden versuchen, die bereits erstellten Ebenen wiederzuverwenden. Die Kacheln, die Ebenen benötigen, sind die neueste Kachel und die vorhergehenden Kacheln, sodass wir genug Ebenen haben, um die Ansicht abzudecken.

%Vor%

Beim ersten Mal und immer wenn die Ansicht ausreichend breiter wird, müssen wir neue Ebenen erstellen. Während wir die Ansicht erstellen, teilen wir ihr mit, dass sie den Inhalt oder die Position nicht animieren soll. Andernfalls wird sie standardmäßig animiert.

%Vor%

Um die Kachelebenen zu erstellen, müssen Sie nur den Rahmen der einzelnen Ebenen festlegen:

%Vor%

Natürlich besteht der Trick darin, den Rahmen für jede Ebene zu berechnen. Und die Teile y, width und height sind einfach genug:

%Vor%

Um die x-Koordinate des Rahmens der Kachel zu berechnen, berechnen wir die x-Koordinate des ersten Beispiels in der Kachel:

%Vor%

Die Berechnung der x-Koordinate für eine Probe erfordert ein wenig Nachdenken. Wir möchten, dass das neueste Sample am rechten Rand der Ansicht steht und das zweitletzte Sample links davon steht und so weiter:

%Vor%

Neu zeichnen

Jetzt können wir darüber sprechen, wie man Fliesen wirklich zeichnet. Wir werden die Zeichnung in einer separaten GCD-Warteschlange erstellen. Wir können nicht sicher auf die meisten Cocoa Touch-Objekte von zwei Threads gleichzeitig zugreifen, daher müssen wir hier vorsichtig sein. Wir verwenden ein Präfix von kPointsPerSample für alle Methoden, die auf rq_ ausgeführt werden, um uns daran zu erinnern, dass wir nicht im Hauptthread sind.

Um eine Kachel neu zu zeichnen, müssen wir die Kachelnummer, die grafischen Grenzen der Kachel und die zu zeichnenden Punkte ermitteln. All diese Dinge stammen aus Datenstrukturen, die wir möglicherweise im Hauptthread ändern, sodass wir nur auf den Hauptthread zugreifen müssen. Also schicken wir zurück zur Hauptwarteschlange:

%Vor%

Zufällig haben wir vielleicht keine Kacheln, die neu gezeichnet werden könnten. Wenn Sie zurück auf _redrawQueue schauen, werden Sie sehen, dass es normalerweise versucht, dieselbe Kachel zweimal in die Warteschlange zu stellen. Da queueTilesForRedrawIfAffectedByLastSample ein Satz (kein Array) ist, wurde das Duplikat verworfen, aber _tilesToRedraw wurde trotzdem zweimal ausgegeben. Also müssen wir überprüfen, ob wir tatsächlich eine Kachel zum Neuzeichnen haben:

%Vor%

Jetzt müssen wir tatsächlich die Beispiele der Kachel zeichnen:

%Vor%

Schließlich müssen wir die Ebene der Kachel aktualisieren, um das neue Bild anzuzeigen. Wir können nur eine Ebene im Hauptthread berühren:

%Vor%

So zeichnen wir das Bild für die Ebene. Ich nehme an, Sie wissen genug Core Graphics, um dies zu folgen:

%Vor%

Aber wir müssen immer noch die Kachel, die Grafikbegrenzungen und die zu zeichnenden Punkte holen. Wir haben es zurück zum Hauptthread geschickt:

%Vor%

Die Grafikgrenzen sind nur die Grenzen der Kachel, genau wie wir es zuvor für den Rahmen der Ebene berechnet haben:

%Vor%

Ich muss mit den Padding-Samples vor dem ersten Sample der Kachel beginnen. Bevor jedoch genügend Samples zur Verfügung stehen, um die Ansicht zu füllen, kann meine Fliesennummer tatsächlich negativ sein! Also muss ich sicher sein, dass ich nicht versuche, auf ein Sample mit einem negativen Index zuzugreifen:

%Vor%

Wir müssen auch sicherstellen, dass wir nicht versuchen, über das Ende der Samples hinauszulaufen, wenn wir das Sample berechnen, bei dem wir aufhören zu zeichnen:

%Vor%

Und wenn ich tatsächlich auf die Sample-Werte zugreife, muss ich die Samples berücksichtigen, die ich verworfen habe:

%Vor%

Nun können wir die tatsächlichen Punkte zum Graphen berechnen:

%Vor%

Und ich kann die Anzahl der Punkte und die Kachel zurückgeben:

%Vor%

So ziehen wir eine Kachel aus der Redraw-Warteschlange. Denken Sie daran, dass die Warteschlange möglicherweise leer ist:

%Vor%

Und schließlich stellen wir hier den Inhalt der Kachelebene auf das neue Bild. Denken Sie daran, dass wir dafür in die Hauptwarteschlange zurückgeschickt wurden:

%Vor%

macht es sexier

Wenn Sie das alles machen, wird es gut funktionieren. Aber Sie können es tatsächlich etwas schöner machen, indem Sie die Neupositionierung der Ebenen animieren, wenn ein neues Muster eingeht. Das ist sehr einfach. Wir ändern nur rq_redrawOneTile so, dass eine Animation für die Eigenschaft newTileLayer hinzugefügt wird:

%Vor%

und wir erstellen die Animation so:

%Vor%

Sie sollten die Dauer so einstellen, dass sie der Geschwindigkeit entspricht, mit der neue Proben ankommen.

    
rob mayoff 28.08.2012, 23:14
quelle
3

Sie müssen den gesamten Pfad nicht bei jedem Zeichnen rastern - Sie können ihn als Raster-Bitmap zwischenspeichern. BTW, Ihre Idee mit "Scrolling" ist Standardlösung für eine solche Aufgabe ...

    
Tutankhamen 28.08.2012 15:47
quelle
0

Erstellen Sie einen Bitmap-Kontext auf derselben Höhe wie Ihre Ansicht, aber doppelt so breit. Beginnen Sie, Ihre Punkte in den Kontext zu zeichnen, und erstellen Sie dann in drawRect einen CGImageRef. Die Idee ist, wenn Sie den Bildschirm anfänglich füllen, beginnt Ihr Bild am Anfang. Das Bild, das Sie zeichnen werden, hat die richtige Breite und Höhe, aber die BytesPerRow wird 2x sein (mehr dazu). Sie zeichnen weiterhin neue Punkte, bis Sie zum letzten Punkt kommen - jetzt ist x erschöpft.

Schreiben Sie weiter Punkte in Ihren Kontext, aber wenn Sie das Bild jetzt erstellen, versetzen Sie den Anfangszeiger um ein Pixel. Fahren Sie damit fort, bis Sie 2x Zeilen erstellt haben - Sie befinden sich jetzt ganz am Ende Ihres Kontextes.

Zu diesem Zeitpunkt müssen Sie die "rechte" Seite des Bildes nach links verschieben und die Anzahl der Versetzungen zurücksetzen. Das heißt, Sie müssen memcpy (starOfBitMap, startOfBitMap + BytesPerRow / 2, sizeOfBitMap - BytesPerRow / 2). Im Wesentlichen werden Sie einen sichtbaren Rahmen verschieben.

Wenn Sie jetzt neue Zeilen hinzufügen, wird diese am Ende des ersten Frames angezeigt, und Sie beginnen beim Zeichnen um ein Pixel zu beginnen.

    
David H 28.08.2012 16:37
quelle

Tags und Links