¿Por qué UIBezierPath es más rápido que la ruta de gráficos centrales?

90

Estaba jugando con trazados de dibujo y noté que, al menos en algunos casos, UIBezierPath supera a lo que pensé que sería un equivalente de Core Graphics. El -drawRect:método siguiente crea dos rutas: una UIBezierPath y una CGPath. Los caminos son idénticos excepto por sus ubicaciones, pero acariciar el CGPath lleva aproximadamente el doble de tiempo que acariciar el 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];
}

Ambas rutas usan CGContextStrokePath (), así que creé métodos separados para trazar cada ruta para poder ver el tiempo usado por cada ruta en Instrumentos. A continuación se muestran los resultados típicos (árbol de llamadas invertido); puedes ver que -strokeContext:toma 9.5 segundos, mientras que -strokeUIBezierPath:solo toma 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 UIBezierPath está optimizando de alguna manera la ruta que crea, o estoy creando CGPath de una manera ingenua. ¿Qué puedo hacer para acelerar mi dibujo CGPath?

Caleb
fuente
2
+1 que suena contradictorio.
Grady Player
1
En general, he encontrado que CoreGraphics es muy lento al dibujar líneas, caminos, etc. No tengo idea de por qué, pero principalmente tengo que bajar a OpenGL o usar Cocos2D para un dibujo eficiente. Claro, entiendo que es más rápido, pero realmente no entiendo por qué CG es mucho más lento, considerando que debería estar usando OpenGL.
Accatyyc
4
UIBezierPathes un envoltorio alrededor CGPathRef. ¿Qué pasa si ejecuta ambos, digamos, diez millones de veces, luego toma el promedio, pero no usa Instrumentos sino dos NSDateobjetos antes y después de las operaciones?
1
@WTP, los resultados son consistentes en el dispositivo y en el simulador y no cambian si -drawRect:se llama unas pocas docenas de veces o unos cientos. Lo probé con hasta 80000 segmentos de curva en el simulador (demasiados para el dispositivo). Los resultados son siempre aproximadamente los mismos: CGPath tarda aproximadamente el doble que UIBezierPath, aunque ambos usan CGContextStrokePath () para dibujar. Parece claro que la ruta que construye UIBezierPath es de alguna manera más eficiente que la que creo con CGPathAddCurveToPoint (). Me gustaría saber cómo construir rutas eficientes como lo hace UIBezierPath.
Caleb

Respuestas:

154

Tiene razón en que UIBezierPathes simplemente un contenedor de objetivo-c para Core Graphics y, por lo tanto, funcionará de manera comparable. La diferencia (y la razón de su delta de rendimiento) es su CGContextestado cuando dibuja su CGPathdirectamente es bastante diferente a esa configuración por UIBezierPath. Si miras UIBezierPath, tiene configuraciones para:

  • lineWidth,
  • lineJoinStyle,
  • lineCapStyle,
  • miterLimit y
  • flatness

Al examinar la llamada (desmontaje) a [path stroke], observará que configura el contexto gráfico actual basándose en esos valores anteriores antes de realizar la CGContextStrokePathllamada. Si hace lo mismo antes de dibujar su CGPath, realizará lo mismo:

- (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ánea de instrumentos: Instantánea de instrumentos que muestra el mismo rendimiento

Stuart Carnie
fuente
6
Gracias por tomarse el tiempo de analizar esto y escribir una explicación tan clara. Esta es realmente una gran respuesta.
Caleb
14
+1 para el icono de atari bruce lee ... y posiblemente para la respuesta.
Grady Player
5
Entonces ... la diferencia de rendimiento de 2x fue una o más de las configuraciones de cgcontext - por ejemplo, quizás algo como: "lineWidth de 2.0 funciona peor que lineWidth de 1.0" ...?
Adam
2
FWIW Siempre he encontrado que el ancho de línea de 1.0 es el más rápido; mi suposición es que el inglete se convierte en un problema para anchos> 1px.
Mark Aufflick