Carga de imágenes no perezosa en iOS

Estoy tratando de cargar UIImages en un hilo de fondo y luego mostrarlos en el iPad. Sin embargo, hay un tartamudeo cuando configuro la propiedad de vista de imageViews a la imagen. Pronto descubrí que la carga de imágenes es lenta en iOS, y encontré una solución parcial en esta pregunta:

CGImage / UIImagez cargándose perezosamente en el hilo de la interfaz de usuario causa tartamudeo

En realidad, esto obliga a que la imagen se cargue en el hilo, pero todavía hay un tartamudeo cuando se muestra la imagen.

Puede encontrar mi proyecto de muestra aquí: http://www.jasamer.com/files/SwapTest.zip (edición: versión fija ), consulte SwapTestViewController. Intenta arrastrar la imagen para ver el tartamudeo.

El código de prueba que creé que tartamudea es este (el método forceLoad es el que se tomó de la pregunta de desbordamiento de stack que publiqué arriba):

NSArray* imagePaths = [NSArray arrayWithObjects: [[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil], [[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil]; NSOperationQueue* queue = [[NSOperationQueue alloc] init]; [queue addOperationWithBlock: ^(void) { int imageIndex = 0; while (true) { UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]]; imageIndex = (imageIndex+1)%2; [image forceLoad]; //What's missing here? [self performSelectorOnMainThread: @selector(setImage:) withObject: image waitUntilDone: YES]; [image release]; } }]; 

Hay dos razones por las que sé que se puede evitar el tartamudeo:

(1) Apple puede cargar imágenes sin tartamudear en la aplicación Fotos

(2) Este código no causa tartamudeo después de que el marcador de posición1 y el marcador de posición2 se hayan mostrado una vez en esta versión modificada del código anterior:

  UIImage* placeholder1 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil]]; [placeholder1 forceLoad]; UIImage* placeholder2 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil]]; [placeholder2 forceLoad]; NSArray* imagePaths = [NSArray arrayWithObjects: [[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil], [[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil]; NSOperationQueue* queue = [[NSOperationQueue alloc] init]; [queue addOperationWithBlock: ^(void) { int imageIndex = 0; while (true) { //The image is not actually used here - just to prove that the background thread isn't causing the stutter UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]]; imageIndex = (imageIndex+1)%2; [image forceLoad]; if (self.imageView.image==placeholder1) { [self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder2 waitUntilDone: YES]; } else { [self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder1 waitUntilDone: YES]; } [image release]; } }]; 

Sin embargo, no puedo mantener todas mis imágenes en la memoria.

Esto implica que forceLoad no hace el trabajo completo; hay algo más que sucede antes de que realmente se muestren las imágenes. ¿Alguien sabe qué es eso y cómo puedo poner eso en el hilo de fondo?

Gracias, Julian

Actualizar

Usé algunos de los consejos de Tommys. Lo que descubrí es que es CGSConvertBGRA8888toRGBA8888 lo que está tomando mucho tiempo, por lo que parece que es una conversión de color lo que está causando el desfase. Aquí está la stack de llamadas (invertida) de ese método.

 Running Symbol Name 6609.0ms CGSConvertBGRA8888toRGBA8888 6609.0ms ripl_Mark 6609.0ms ripl_BltImage 6609.0ms RIPLayerBltImage 6609.0ms ripc_RenderImage 6609.0ms ripc_DrawImage 6609.0ms CGContextDelegateDrawImage 6609.0ms CGContextDrawImage 6609.0ms CA::Render::create_image_by_rendering(CGImage*, CGColorSpace*, bool) 6609.0ms CA::Render::create_image(CGImage*, CGColorSpace*, bool) 6609.0ms CA::Render::copy_image(CGImage*, CGColorSpace*, bool) 6609.0ms CA::Render::prepare_image(CGImage*, CGColorSpace*, bool) 6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*) 6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*) 6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*) 6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*) 6609.0ms CALayerPrepareCommit 6609.0ms CA::Context::commit_transaction(CA::Transaction*) 6609.0ms CA::Transaction::commit() 6609.0ms CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) 6609.0ms __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 6609.0ms __CFRunLoopDoObservers 6609.0ms __CFRunLoopRun 6609.0ms CFRunLoopRunSpecific 6609.0ms CFRunLoopRunInMode 6609.0ms GSEventRunModal 6609.0ms GSEventRun 6609.0ms -[UIApplication _run] 6609.0ms UIApplicationMain 6609.0ms main 

Los últimos cambios de máscara de bits que propuso no cambiaron nada, por desgracia.

UIKit solo se puede usar en el hilo principal. Por lo tanto, su código es técnicamente inválido, ya que usa UIImage de un hilo que no sea el hilo principal. Debes usar CoreGraphics solo para cargar (y decodificar no perezosamente) gráficos en un hilo de fondo, publicar el CGImageRef en el hilo principal y convertirlo en un UIImage allí. Puede parecer que funciona (aunque con el tartamudeo que no desea) en su implementación actual, pero no está garantizado. Parece que hay muchas supersticiones y malas prácticas defendidas en esta área, por lo que no es sorprendente que hayas encontrado algunos malos consejos …

Recomendado para ejecutarse en un hilo de fondo:

 // get a data provider referencing the relevant file CGDataProviderRef dataProvider = CGDataProviderCreateWithFilename(filename); // use the data provider to get a CGImage; release the data provider CGImageRef image = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault); CGDataProviderRelease(dataProvider); // make a bitmap context of a suitable size to draw to, forcing decode size_t width = CGImageGetWidth(image); size_t height = CGImageGetHeight(image); unsigned char *imageBuffer = (unsigned char *)malloc(width*height*4); CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef imageContext = CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little); CGColorSpaceRelease(colourSpace); // draw the image to the context, release it CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image); CGImageRelease(image); // now get an image ref from the context CGImageRef outputImage = CGBitmapContextCreateImage(imageContext); // post that off to the main thread, where you might do something like // [UIImage imageWithCGImage:outputImage] [self performSelectorOnMainThread:@selector(haveThisImage:) withObject:[NSValue valueWithPointer:outputImage] waitUntilDone:YES]; // clean up CGImageRelease(outputImage); CGContextRelease(imageContext); free(imageBuffer); 

No es necesario hacer malloc / free si tiene iOS 4 o posterior, puede pasar NULL como el parámetro relevante de CGBitmapContextCreate, y dejar que CoreGraphics resuelva su propio almacenamiento.

Esto difiere de la solución que publica porque:

  1. crea una CGImage desde una fuente de datos PNG – se aplica la carga diferida, por lo que esta no es necesariamente una imagen completamente cargada y descomprimida
  2. crea un contexto de bitmap del mismo tamaño que el archivo PNG
  3. dibuja CGImage desde el origen de datos PNG en el contexto del bitmap; esto debería forzar la carga y descompresión total, ya que los valores de color reales deben ubicarse en algún lugar donde podamos acceder a ellos desde una matriz C. Este paso es tan lejos como va el ForceLoad al que enlazas.
  4. convierte el contexto del bitmap en una imagen
  5. publica esa imagen en el hilo principal, presumiblemente para convertirse en un UIImage

Entonces no hay continuidad de objeto entre lo que se carga y lo que se muestra; los datos de los píxeles pasan por una matriz C (por lo tanto, no hay oportunidad para engaños ocultos) y solo si se colocaron correctamente en la matriz es posible hacer la imagen final.

Ok, lo descubrí, con mucha ayuda de Tommy. ¡Gracias!

Si creas tu contexto con

  CGContextRef imageContext = CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little); 

el ciclo de ejecución principal ya no generará conversiones en el hilo principal. Mostrar ahora es suave como la mantequilla. (Las banderas son un poco contradictorias, ¿alguien sabe por qué tiene que elegir kCGImageAlphaPremultipliedFirst?)

Editar:

Subido el proyecto de muestra fijo: http://www.jasamer.com/files/SwapTest-Fixed.zip . Si tiene problemas con el rendimiento de la imagen, ¡este es un gran punto de partida!

¿Qué hay de UIImage + ImmediateLoad.m ?

Decodifica imágenes de inmediato. Además de UIImage -initwithContentsOfFile :, -imageWithCGImage: y CG * son seguros para subprocesos después de iOS4.