Warum ist UIBezierPath schneller als der Core Graphics-Pfad?

90

Ich habe mit Zeichenpfaden herumgespielt und festgestellt, dass UIBezierPath zumindest in einigen Fällen das übertrifft, was ich für ein Core Graphics-Äquivalent hielt. Die folgende -drawRect:Methode erstellt zwei Pfade: einen UIBezierPath und einen CGPath. Die Pfade sind bis auf ihre Position identisch, aber das Streicheln des CGPath dauert ungefähr doppelt so lange wie das Streichen des UIBezierPath.

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    // Create the two paths, cgpath and uipath.
    CGMutablePathRef cgpath = CGPathCreateMutable();
    CGPathMoveToPoint(cgpath, NULL, 0, 100);

    UIBezierPath *uipath = [[UIBezierPath alloc] init];
    [uipath moveToPoint:CGPointMake(0, 200)];

    // Add 200 curve segments to each path.
    int iterations = 200;
    CGFloat cgBaseline = 100;
    CGFloat uiBaseline = 200;
    CGFloat xincrement = self.bounds.size.width / iterations;
    for (CGFloat x1 = 0, x2 = xincrement;
         x2 < self.bounds.size.width;
         x1 = x2, x2 += xincrement)
    {
        CGPathAddCurveToPoint(cgpath, NULL, x1, cgBaseline-50, x2, cgBaseline+50, x2, cgBaseline);
        [uipath addCurveToPoint:CGPointMake(x2, uiBaseline)
                  controlPoint1:CGPointMake(x1, uiBaseline-50)
                  controlPoint2:CGPointMake(x2, uiBaseline+50)];
    }
    [[UIColor blackColor] setStroke];
    CGContextAddPath(ctx, cgpath);

    // Stroke each path.
    [self strokeContext:ctx];
    [self strokeUIBezierPath:uipath];

    [uipath release];
    CGPathRelease(cgpath);
}

- (void)strokeContext:(CGContextRef)context
{
    CGContextStrokePath(context);
}

- (void)strokeUIBezierPath:(UIBezierPath*)path
{
    [path stroke];
}

Beide Pfade verwenden CGContextStrokePath (). Daher habe ich separate Methoden erstellt, um jeden Pfad zu streicheln, damit ich die von jedem Pfad in Instruments verwendete Zeit sehen kann. Nachfolgend sind typische Ergebnisse aufgeführt (Aufrufbaum invertiert). Sie können sehen, dass dies -strokeContext:9,5 Sekunden -strokeUIBezierPath:dauert , während es nur 5 Sekunden dauert:

Running (Self)      Symbol Name
14638.0ms   88.2%               CGContextStrokePath
9587.0ms   57.8%                 -[QuartzTestView strokeContext:]
5051.0ms   30.4%                 -[UIBezierPath stroke]
5051.0ms   30.4%                  -[QuartzTestView strokeUIBezierPath:]

Es sieht so aus, als würde UIBezierPath den von ihm erstellten Pfad irgendwie optimieren, oder ich erstelle den CGPath auf naive Weise. Was kann ich tun, um meine CGPath-Zeichnung zu beschleunigen?

Caleb
quelle
2
+1 das klingt kontraintuitiv.
Grady Player
1
Ich habe allgemein festgestellt, dass CoreGraphics beim Zeichnen von Linien, Pfaden usw. sehr langsam ist. Ich habe keine Ahnung warum, aber ich muss meistens zu OpenGL gehen oder Cocos2D verwenden, um effizient zu zeichnen. Sicher verstehe ich, dass es schneller ist, aber ich verstehe nicht wirklich, warum CG so viel langsamer ist, wenn man bedenkt, dass es OpenGL selbst verwenden sollte.
Accatyyc
4
UIBezierPathist ein Wrapper herum CGPathRef. Was ist, wenn Sie beide ausführen, sagen wir zehn Millionen Mal, und dann den Durchschnitt nehmen, aber keine Instrumente verwenden, sondern zwei NSDateObjekte vor und nach den Operationen.
1
@WTP, die Ergebnisse sind auf dem Gerät und im Simulator konsistent und ändern nichts daran, ob -drawRect:sie ein paar Dutzend Mal oder ein paar Hundert Mal aufgerufen werden. Ich habe es mit bis zu 80000 Kurvensegmenten auf dem Simulator versucht (viel zu viele für das Gerät). Die Ergebnisse sind immer ungefähr gleich: CGPath dauert ungefähr doppelt so lange wie UIBezierPath, obwohl beide CGContextStrokePath () zum Zeichnen verwenden. Es scheint klar zu sein, dass der Pfad, den UIBezierPath erstellt, irgendwie effizienter ist als der, den ich mit CGPathAddCurveToPoint () erstellt habe. Ich möchte wissen, wie man effiziente Pfade wie UIBezierPath erstellt.
Caleb

Antworten:

154

Sie haben Recht damit, dass UIBezierPathes sich lediglich um einen Objective-C-Wrapper für Core Graphics handelt und daher eine vergleichbare Leistung erbringt. Der Unterschied (und der Grund für Ihr Leistungsdelta) ist Ihr CGContextStatus beim CGPathdirekten Zeichnen , der sich erheblich von dem von unterscheidet UIBezierPath. Wenn Sie sich ansehen UIBezierPath, hat es Einstellungen für:

  • lineWidth,
  • lineJoinStyle,
  • lineCapStyle,
  • miterLimit und
  • flatness

Wenn Sie den Aufruf (Demontage) untersuchen [path stroke], werden Sie feststellen, dass der aktuelle grafische Kontext basierend auf diesen vorherigen Werten konfiguriert wird, bevor der CGContextStrokePathAufruf ausgeführt wird. Wenn Sie vor dem Zeichnen Ihres CGPath dasselbe tun, wird dasselbe ausgeführt:

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    // Create the two paths, cgpath and uipath.
    CGMutablePathRef cgpath = CGPathCreateMutable();
    CGPathMoveToPoint(cgpath, NULL, 0, 100);

    UIBezierPath *uipath = [[UIBezierPath alloc] init];
    [uipath moveToPoint:CGPointMake(0, 200)];

    // Add 200 curve segments to each path.
    int iterations = 80000;
    CGFloat cgBaseline = 100;
    CGFloat uiBaseline = 200;
    CGFloat xincrement = self.bounds.size.width / iterations;
    for (CGFloat x1 = 0, x2 = xincrement;
         x2 < self.bounds.size.width;
         x1 = x2, x2 += xincrement)
    {
        CGPathAddCurveToPoint(cgpath, NULL, x1, cgBaseline-50, x2, cgBaseline+50, x2, cgBaseline);
        [uipath addCurveToPoint:CGPointMake(x2, uiBaseline)
                  controlPoint1:CGPointMake(x1, uiBaseline-50)
                  controlPoint2:CGPointMake(x2, uiBaseline+50)];
    }
    [[UIColor blackColor] setStroke];
    CGContextAddPath(ctx, cgpath);

    // Stroke each path
    CGContextSaveGState(ctx); {
        // configure context the same as uipath
        CGContextSetLineWidth(ctx, uipath.lineWidth);
        CGContextSetLineJoin(ctx, uipath.lineJoinStyle);
        CGContextSetLineCap(ctx, uipath.lineCapStyle);
        CGContextSetMiterLimit(ctx, uipath.miterLimit);
        CGContextSetFlatness(ctx, uipath.flatness);
        [self strokeContext:ctx];
        CGContextRestoreGState(ctx);
    }
    [self strokeUIBezierPath:uipath];

    [uipath release];
    CGPathRelease(cgpath);
}

- (void)strokeContext:(CGContextRef)context
{
    CGContextStrokePath(context);
}

- (void)strokeUIBezierPath:(UIBezierPath*)path
{
    [path stroke];
}

Schnappschuss von Instrumenten: Schnappschuss von Instrumenten mit gleicher Leistung

Stuart Carnie
quelle
6
Vielen Dank, dass Sie sich die Zeit genommen haben, dies zu untersuchen und eine so klare Erklärung zu schreiben. Das ist wirklich eine gute Antwort.
Caleb
14
+1 für das Atari Bruce Lee Symbol ... und möglicherweise für die Antwort.
Grady Player
5
Also ... der 2x Leistungsunterschied war eine oder mehrere der cgcontext-Einstellungen - zB vielleicht so etwas wie: "lineWidth of 2.0 ist schlechter als lineWidth of 1.0" ...?
Adam
2
FWIW Ich habe immer festgestellt, dass die Linienbreite von 1,0 am schnellsten ist. Ich gehe davon aus, dass Gehrung bei Breiten> 1 Pixel zu einem Problem wird.
Mark Aufflick