Forma más eficiente de dibujar parte de una image en iOS

Dado un UIImage y un CGRect , ¿cuál es la forma más eficiente (en memory y time) de dibujar la parte de la image correspondiente a CGRect (sin escala)?

Como reference, así es como lo hago actualmente:

 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGRect frameRect = CGRectMake(frameOrigin.x + rect.origin.x, frameOrigin.y + rect.origin.y, rect.size.width, rect.size.height); CGImageRef imageRef = CGImageCreateWithImageInRect(image_.CGImage, frameRect); CGContextTranslateCTM(context, 0, rect.size.height); CGContextScaleCTM(context, 1.0, -1.0); CGContextDrawImage(context, rect, imageRef); CGImageRelease(imageRef); } 

Lamentablemente, esto parece extremadamente lento con imágenes de tamaño medio y una alta frecuencia de setNeedsDisplay . Jugar con el marco y clipToBounds produce mejores resultados (con less flexibilidad).

Supuse que estás haciendo esto para mostrar parte de una image en la pantalla, porque mencionaste UIImageView . Y los problemas de optimization siempre deben definirse específicamente.


Confíe en Apple para las cosas normales de UI

En realidad, UIImageView con clipsToBounds es una de las forms más rápidas / más simples de archivar su objective si su objective es simplemente recortar una región rectangular de una image (no demasiado grande). Además, no es necesario enviar el post setNeedsDisplay .

O puede intentar colocar el UIImageView dentro de una UIView vacía y establecer el recorte en la vista del contenedor. Con esta técnica, puede transformar su image libremente al establecer la propiedad de transform en 2D (escala, rotation, traducción).

Si necesita transformación en 3D, aún puede usar CALayer con la propiedad masksToBounds , pero al usar CALayer le dará muy poco performance adicional, por lo general no es considerable.

De todos modos, debe conocer todos los detalles de bajo nivel para usarlos correctamente para la optimization.


¿Por qué es esa una de las forms más rápidas?

UIView es solo una capa delgada en la parte superior de CALayer que se implementa en la parte superior de OpenGL, que es una interfaz virtualmente directa a la GPU . Esto significa que UIKit está siendo acelerado por la GPU.

Entonces, si los usa correctamente (es decir, dentro de las limitaciones diseñadas), funcionará tan bien como la simple implementación de OpenGL . Si utiliza solo unas pocas imágenes para mostrar, obtendrá un performance aceptable con la implementación de UIView porque puede get una aceleración total del OpenGL subyacente (lo que significa aceleración de la GPU).

De todos modos, si necesita una optimization extrema para cientos de sprites animados con sombreadores de píxeles finamente sintonizados como en una aplicación de juego, debe usar OpenGL directamente, porque CALayer carece de muchas opciones para la optimization en niveles inferiores. De todos modos, al less para la optimization de la interfaz de usuario, es increíblemente difícil ser mejor que Apple.


¿Por qué su método es más lento que UIImageView ?

Lo que debe saber es la aceleración de la GPU. En todas las computadoras recientes, el rápido performance de los charts se logra solo con la GPU. Entonces, el punto es si el método que está utilizando se implementa en la parte superior de la GPU o no.

IMO, CGImage methods de dibujo de CGImage no se implementan con la GPU. Creo que leí mencionar sobre esto en la documentation de Apple, pero no recuerdo dónde. Entonces no estoy seguro de esto. De todos modos, creo que CGImage está implementado en la CPU porque,

  1. Su API parece que fue diseñado para la CPU, como la interfaz de edición de maps de bits y el dibujo de text. No encajan muy bien en una interfaz GPU.
  2. La interfaz de context Bitmap permite el acceso directo a la memory. Eso significa que su almacenamiento de backend se encuentra en la memory de la CPU. Tal vez algo diferente en la architecture de memory unificada (y también con Metal API), pero de todos modos, la intención de layout inicial de CGImage debería ser para la CPU.
  3. Muchas otras API de Apple lanzadas recientemente mencionan explícitamente la aceleración de la GPU. Eso significa que sus API más antiguas no lo fueron. Si no hay una mención especial, generalmente se hace en la CPU por defecto.

Entonces, parece que se hace en la CPU. Las operaciones gráficas realizadas en la CPU son mucho más lentas que en la GPU.

Simplemente el recorte de una image y la composition de las capas de la image son operaciones muy simples y baratas para la GPU (en comparación con la CPU), por lo que puede esperar que la biblioteca UIKit lo use porque el UIKit completo se implementa sobre OpenGL.

  • Aquí hay otro hilo sobre si CoreGraphics en iOS está usando OpenGL o no: iOS: ¿se ha implementado Core Graphics además de OpenGL?

Sobre las limitaciones

Debido a que la optimization es un tipo de trabajo sobre la microgestión, los numbers específicos y los pequeños hechos son muy importantes. ¿Cuál es el tamaño mediano ? OpenGL en iOS generalmente limita el tamaño máximo de textura a 1024×1024 píxeles (quizás más grande en versiones recientes). Si su image es más grande que esto, no funcionará, o el performance se degradará en gran medida (creo que UIImageView está optimizado para imágenes dentro de los límites).

Si necesita mostrar imágenes enormes con recorte, debe usar otra optimization como CATiledLayer y esa es una historia totalmente diferente.

Y no vayas OpenGL a less que quieras saber todos los detalles de OpenGL. Necesita una comprensión completa sobre los charts de bajo nivel y al less 100 veces más código.


Sobre un futuro

Aunque no es muy probable que suceda, pero CGImage materias de CGImage (o cualquier otra cosa) no necesitan estar atascadas solo en la CPU. No olvides verificar la tecnología base de la API que estás utilizando. Aun así, las cosas de la GPU son monstruos muy diferentes de la CPU, entonces los chicos de la API generalmente los mencionan explícita y claramente.

En última instancia, sería más rápido, con mucho less creación de imágenes de atlas sprites, si pudiera configurar no solo la image para una UIImageView, sino también la compensación superior izquierda para mostrar dentro de ese UIImage. Quizás esto sea posible.

Mientras tanto, creé estas funciones útiles en una class de utilidad que uso en mis aplicaciones. Crea un UIImage de parte de otro UIImage, con opciones para rotar, escalar y voltear usando valores de UIImageOrientation estándar para especificar.

Mi aplicación crea una gran cantidad de UIImages durante la initialization, y esto necesariamente toma time. Pero algunas imágenes no son necesarias hasta que se select una pestaña determinada. Para dar la apariencia de una carga más rápida, podría crearlos en un hilo separado engendrado en el inicio, y luego esperar hasta que se haga si se selecciona esa pestaña.

 + (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture { return [ChordCalcController imageByCropping:imageToCrop toRect:aperture withOrientation:UIImageOrientationUp]; } // Draw a full image into a crop-sized area and offset to produce a cropped, rotated image + (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture withOrientation:(UIImageOrientation)orientation { // convert y coordinate to origin bottom-left CGFloat orgY = aperture.origin.y + aperture.size.height - imageToCrop.size.height, orgX = -aperture.origin.x, scaleX = 1.0, scaleY = 1.0, rot = 0.0; CGSize size; switch (orientation) { case UIImageOrientationRight: case UIImageOrientationRightMirronetworking: case UIImageOrientationLeft: case UIImageOrientationLeftMirronetworking: size = CGSizeMake(aperture.size.height, aperture.size.width); break; case UIImageOrientationDown: case UIImageOrientationDownMirronetworking: case UIImageOrientationUp: case UIImageOrientationUpMirronetworking: size = aperture.size; break; default: assert(NO); return nil; } switch (orientation) { case UIImageOrientationRight: rot = 1.0 * M_PI / 2.0; orgY -= aperture.size.height; break; case UIImageOrientationRightMirronetworking: rot = 1.0 * M_PI / 2.0; scaleY = -1.0; break; case UIImageOrientationDown: scaleX = scaleY = -1.0; orgX -= aperture.size.width; orgY -= aperture.size.height; break; case UIImageOrientationDownMirronetworking: orgY -= aperture.size.height; scaleY = -1.0; break; case UIImageOrientationLeft: rot = 3.0 * M_PI / 2.0; orgX -= aperture.size.height; break; case UIImageOrientationLeftMirronetworking: rot = 3.0 * M_PI / 2.0; orgY -= aperture.size.height; orgX -= aperture.size.width; scaleY = -1.0; break; case UIImageOrientationUp: break; case UIImageOrientationUpMirronetworking: orgX -= aperture.size.width; scaleX = -1.0; break; } // set the draw rect to pan the image to the right spot CGRect drawRect = CGRectMake(orgX, orgY, imageToCrop.size.width, imageToCrop.size.height); // create a context for the new image UIGraphicsBeginImageContextWithOptions(size, NO, imageToCrop.scale); CGContextRef gc = UIGraphicsGetCurrentContext(); // apply rotation and scaling CGContextRotateCTM(gc, rot); CGContextScaleCTM(gc, scaleX, scaleY); // draw the image to our clipped context using the offset rect CGContextDrawImage(gc, drawRect, imageToCrop.CGImage); // pull the image from our cropped context UIImage *cropped = UIGraphicsGetImageFromCurrentImageContext(); // pop the context to get back to the default UIGraphicsEndImageContext(); // Note: this is autoreleased return cropped; } 

La forma muy simple de mover una gran image dentro de UIImageView de la siguiente manera.

Dejemos que tengamos la image de tamaño (100, 400) representando 4 estados de alguna image uno debajo de otro. Queremos mostrar la segunda image con offsetY = 100 en UIImageView de tamaño cuadrado (100, 100). La solucion es:

 UIImageView *iView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; CGRect contentFrame = CGRectMake(0, 0.25, 1, 0.25); iView.layer.contentsRect = contentFrame; iView.image = [UIImage imageNamed:@"NAME"]; 

Aquí, contentFrame se normaliza en relación con el tamaño real de UIImage . Entonces, "0" significa que comenzamos una parte visible de la image desde el borde izquierdo, "0.25" significa que tenemos un desplazamiento vertical 100, "1" significa que queremos mostrar el ancho completo de la image y, por último, "0.25" significa que queremos mostrar solo 1/4 parte de la image en altura.

Por lo tanto, en las coorderadas de image locales, mostramos el siguiente cuadro

 CGRect visibleAbsoluteFrame = CGRectMake(0*100, 0.25*400, 1*100, 0.25*400) or CGRectMake(0, 100, 100, 100); 

En lugar de crear una nueva image (que es costosa porque asigna memory), ¿qué hay de usar CGContextClipToRect ?

La forma más rápida es utilizar una máscara de image: una image que tiene el mismo tamaño que la image que se va a enmascarar, pero con un cierto patrón de píxeles que indica qué parte de la image se debe enmascarar cuando se renderiza ->

 // maskImage is used to block off the portion that you do not want rendenetworking // note that rect is not actually used because the image mask defines the rect that is rendenetworking -(void) drawRect:(CGRect)rect maskImage:(UIImage*)maskImage { UIGraphicsBeginImageContext(image_.size); [maskImage drawInRect:image_.bounds]; maskImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); CGImageRef maskRef = maskImage.CGImage; CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef), CGImageGetHeight(maskRef), CGImageGetBitsPerComponent(maskRef), CGImageGetBitsPerPixel(maskRef), CGImageGetBytesPerRow(maskRef), CGImageGetDataProvider(maskRef), NULL, false); CGImageRef maskedImageRef = CGImageCreateWithMask([image_ CGImage], mask); image_ = [UIImage imageWithCGImage:maskedImageRef scale:1.0f orientation:image_.imageOrientation]; CGImageRelease(mask); CGImageRelease(maskedImageRef); }