Por que o UIBezierPath é mais rápido que o caminho do Core Graphics?

90

Eu estava brincando com o desenho de caminhos e percebi que, pelo menos em alguns casos, o UIBezierPath supera o que eu pensei que seria um equivalente do Core Graphics. O -drawRect:método a seguir cria dois caminhos: um UIBezierPath e um CGPath. Os caminhos são idênticos, exceto por suas localizações, mas acariciar o CGPath leva aproximadamente o dobro do tempo que acariciar o 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];
}

Ambos os caminhos usam CGContextStrokePath (), então criei métodos separados para traçar cada caminho para que eu possa ver o tempo usado por cada caminho em Instrumentos. Abaixo estão os resultados típicos (árvore de chamadas invertida); você pode ver que -strokeContext:leva 9,5 segundos, enquanto -strokeUIBezierPath:leva apenas 5 segundos:

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

Parece que o UIBezierPath está de alguma forma otimizando o caminho que ele cria, ou estou criando o CGPath de uma forma ingênua. O que posso fazer para acelerar meu desenho CGPath?

Caleb
fonte
2
+1 que soa contra-intuitivo.
Grady Player
1
Eu geralmente achei CoreGraphics muito lento ao desenhar linhas, caminhos etc. Não tenho ideia do porquê, mas principalmente tenho que descer para OpenGL ou usar o Cocos2D para um desenho eficiente. Claro que entendo que é mais rápido, mas realmente não entendo porque CG é tão mais lento, considerando que deveria estar usando o próprio OpenGL.
Accatyyc
4
UIBezierPathé um invólucro CGPathRef. E se você executar ambos, digamos, dez milhões de vezes, e então tirar a média, mas não usar instrumentos, mas dois NSDateobjetos antes e depois das operações.
1
@WTP, os resultados são consistentes no dispositivo e no simulador e não mudam se -drawRect:é chamado algumas dezenas de vezes ou algumas centenas. Eu tentei com até 80.000 segmentos de curva no simulador (muitos para o dispositivo). Os resultados são sempre os mesmos: CGPath leva cerca de duas vezes mais que UIBezierPath, embora ambos usem CGContextStrokePath () para desenhar. Parece claro que o caminho que UIBezierPath constrói é de alguma forma mais eficiente do que aquele que crio com CGPathAddCurveToPoint (). Gostaria de saber como construir caminhos eficientes como o UIBezierPath faz.
Caleb

Respostas:

154

Você está correto em que UIBezierPathé simplesmente um wrapper objetivo-c para Core Graphics e, portanto, terá um desempenho comparável. A diferença (e a razão para seu delta de desempenho) é que seu CGContextestado ao desenhar CGPathdiretamente é bem diferente daquela configuração por UIBezierPath. Se você olhar UIBezierPath, ele tem configurações para:

  • lineWidth,
  • lineJoinStyle,
  • lineCapStyle,
  • miterLimit e
  • flatness

Ao examinar a chamada (desmontagem) para [path stroke], você notará que ela configura o contexto gráfico atual com base nos valores anteriores antes de realizar a CGContextStrokePathchamada. Se você fizer o mesmo antes de desenhar seu CGPath, ele terá o mesmo desempenho:

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

Instantâneo de instrumentos: Instantâneo de instrumentos mostrando desempenho igual

Stuart Carnie
fonte
6
Obrigado por dedicar seu tempo para analisar isso e escrever uma explicação tão clara. Esta é realmente uma ótima resposta.
Caleb de
14
+1 para o ícone atari bruce lee ... e possivelmente para a resposta.
Grady Player
5
Então ... a diferença de desempenho 2x foi uma ou mais das configurações cgcontext - por exemplo, talvez algo como: "lineWidth de 2.0 tem desempenho pior do que lineWidth de 1.0" ...?
Adam
2
FWIW Sempre achei que a largura de linha de 1,0 é a mais rápida - suponho que a mitigação se torne um problema para larguras> 1px.
Mark Aufflick