Dlaczego ścieżka UIBezierPath jest szybsza niż ścieżka grafiki podstawowej?

90

Bawiłem się rysowaniem ścieżek i zauważyłem, że przynajmniej w niektórych przypadkach UIBezierPath przewyższa to, co uważałem za odpowiednik Core Graphics. Poniższa -drawRect:metoda tworzy dwie ścieżki: jedną UIBezierPath i jedną CGPath. Ścieżki są identyczne z wyjątkiem ich lokalizacji, ale głaskanie CGPath trwa około dwa razy dłużej niż głaskanie 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];
}

Obie ścieżki używają CGContextStrokePath (), więc stworzyłem oddzielne metody obrysowujące każdą ścieżkę, aby zobaczyć czas używany przez każdą ścieżkę w Instruments. Poniżej znajdują się typowe wyniki (odwrócone drzewo wywołań); widać, że -strokeContext:zajmuje to 9,5 sek., a -strokeUIBezierPath:zajmuje tylko 5 sek .:

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:]

Wygląda na to, że UIBezierPath w jakiś sposób optymalizuje ścieżkę, którą tworzy, albo tworzę CGPath w naiwny sposób. Co mogę zrobić, aby przyspieszyć rysowanie CGPath?

Caleb
źródło
2
+1, co brzmi przeciwnie.
Grady Player
1
Ogólnie uważam, że CoreGraphics działa bardzo wolno podczas rysowania linii, ścieżek itp. Nie mam pojęcia, dlaczego, ale głównie muszę przejść do OpenGL lub użyć Cocos2D do wydajnego rysowania. Jasne, rozumiem, że jest szybszy, ale tak naprawdę nie rozumiem, dlaczego CG jest tak dużo wolniejsze, biorąc pod uwagę, że powinno używać samego OpenGL.
Accatyyc
4
UIBezierPathjest opakowaniem wokół CGPathRef. Co jeśli uruchomisz oba, powiedzmy, dziesięć milionów razy, a następnie weź średnią, ale nie używaj Instruments, ale dwóch NSDateobiektów przed i po operacjach.
1
@WTP, wyniki są spójne na urządzeniu iw symulatorze i nie zmieniają się, czy -drawRect:wywoływane jest kilkadziesiąt razy, czy kilkaset. Wypróbowałem to z aż 80000 segmentami krzywych na symulatorze (zdecydowanie za dużo dla urządzenia). Wyniki są zawsze mniej więcej takie same: CGPath zajmuje około dwa razy dłużej niż UIBezierPath, mimo że oba używają CGContextStrokePath () do rysowania. Wydaje się jasne, że ścieżka, którą konstruuje UIBezierPath, jest w jakiś sposób bardziej wydajna niż ta, którą tworzę za pomocą CGPathAddCurveToPoint (). Chciałbym wiedzieć, jak tworzyć wydajne ścieżki, takie jak UIBezierPath.
Caleb

Odpowiedzi:

154

Masz rację w tym, że UIBezierPathjest to po prostu celowe opakowanie dla Core Graphics i dlatego będzie działać porównywalnie. Różnica (i przyczyna twojej delty wydajności) to twój CGContextstan podczas CGPathbezpośredniego rysowania, jest zupełnie inny niż w ustawieniu UIBezierPath. Jeśli spojrzysz na UIBezierPath, ma ustawienia dla:

  • lineWidth,
  • lineJoinStyle,
  • lineCapStyle,
  • miterLimit i
  • flatness

Podczas badania wywołania (dezasemblacji) do [path stroke], zauważysz, że przed wykonaniem CGContextStrokePathpołączenia konfiguruje bieżący kontekst graficzny w oparciu o te poprzednie wartości . Jeśli zrobisz to samo przed narysowaniem ścieżki CGPath, wykona to samo:

- (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];
}

Migawka z instrumentów: Migawka instrumentów pokazująca równą wydajność

Stuart Carnie
źródło
6
Dziękuję za poświęcenie czasu na przyjrzenie się temu i napisanie tak jasnego wyjaśnienia. To naprawdę świetna odpowiedź.
Caleb
14
+1 dla ikony Atari Bruce Lee ... i prawdopodobnie za odpowiedź.
Grady Player
5
Więc ... 2x różnica wydajności wynikała z jednego lub więcej ustawień cgcontext - np. Może coś w stylu: "lineWidth of 2.0 działa gorzej niż lineWidth of 1.0" ...?
Adam
2
FWIW Zawsze uważałem, że szerokość linii 1,0 jest najszybsza - przypuszczam, że przycinanie staje się problemem dla szerokości> 1 piksela.
Mark Aufflick,