¿Cómo get una snappy UICollectionView con muchas imágenes (50-200)?

Estoy usando UICollectionView en una aplicación que muestra muchas fotos (50-200) y estoy teniendo problemas para que sea ágil (tan ágil como la aplicación de Fotos, por ejemplo).

Tengo un UICollectionViewCell personalizado con un UIImageView como su subvista. Estoy cargando imágenes desde el sistema de files, con UIImage.imageWithContentsOfFile: en UIImageViews dentro de las celdas.

He intentado algunos enfoques ahora, pero han tenido errores o problemas de performance.

NOTA: Estoy usando RubyMotion, así que escribiré los fragments de código en el estilo Ruby.

En primer lugar, aquí está mi class UICollectionViewCell personalizada como reference …

 class CustomCell < UICollectionViewCell def initWithFrame(rect) super @image_view = UIImageView.alloc.initWithFrame(self.bounds).tap do |iv| iv.contentMode = UIViewContentModeScaleAspectFill iv.clipsToBounds = true iv.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth self.contentView.addSubview(iv) end self end def prepareForReuse super @image_view.image = nil end def image=(image) @image_view.image = image end end 

Enfoque n. ° 1

Manteniendo las cosas simples …

 def collectionView(collection_view, cellForItemAtIndexPath: index_path) cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path) image_path = @image_paths[index_path.row] cell.image = UIImage.imageWithContentsOfFile(image_path) end 

Desplazarse hacia arriba / abajo es horrible usando esto. Es nervioso y lento.

Enfoque n. ° 2

Agregue un poco de caching a través de NSCache …

 def viewDidLoad ... @images_cache = NSCache.alloc.init ... end def collectionView(collection_view, cellForItemAtIndexPath: index_path) cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path) image_path = @image_paths[index_path.row] if cached_image = @images_cache.objectForKey(image_path) cell.image = cached_image else image = UIImage.imageWithContentsOfFile(image_path) @images_cache.setObject(image, forKey: image_path) cell.image = image end end 

Una vez más, nervioso y lento en el primer desplazamiento completo, pero a partir de ese momento es fácil navegar.

Enfoque n. ° 3

Cargue las imágenes de forma asincrónica … (y guarde el almacenamiento en caching)

 def viewDidLoad ... @images_cache = NSCache.alloc.init @image_loading_queue = NSOperationQueue.alloc.init @image_loading_queue.maxConcurrentOperationCount = 3 ... end def collectionView(collection_view, cellForItemAtIndexPath: index_path) cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path) image_path = @image_paths[index_path.row] if cached_image = @images_cache.objectForKey(image_path) cell.image = cached_image else @operation = NSBlockOperation.blockOperationWithBlock lambda { @image = UIImage.imageWithContentsOfFile(image_path) Dispatch::Queue.main.async do return unless collectionView.indexPathsForVisibleItems.containsObject(index_path) @images_cache.setObject(@image, forKey: image_path) cell = collectionView.cellForItemAtIndexPath(index_path) cell.image = @image end } @image_loading_queue.addOperation(@operation) end end 

Esto parecía prometedor, pero hay un error que no puedo rastrear. En la vista inicial, cargue todas las imágenes en la misma image y, a medida que vaya desplazando bloques integers, cargue con otra image, pero todos tendrán esa image. Intenté depurarlo pero no puedo resolverlo.

También he intentado sustituir el NSOperation con Dispatch::Queue.concurrent.async { ... } pero parece ser el mismo.

Creo que este es probablemente el enfoque correcto si pudiera hacer que funcione correctamente.

Enfoque n. ° 4

En frustración, decidí cargar todas las imágenes como UIImages …

 def viewDidLoad ... @preloaded_images = @image_paths.map do |path| UIImage.imageWithContentsOfFile(path) end ... end def collectionView(collection_view, cellForItemAtIndexPath: index_path) cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path) cell.image = @preloaded_images[index_path.row] end 

Esto resultó ser un poco lento y nervioso en el primer desplazamiento completo pero todo bien después de eso.

Resumen

Entonces, si alguien puede señalarme en la dirección correcta, estaría muy agradecido. ¿Cómo lo hace la aplicación de fotos tan bien ?


Nota para cualquier otra persona: [La respuesta aceptada] resolvió mi problema con las imágenes que aparecen en las celdas incorrectas, pero seguía recibiendo un desplazamiento entrecortado incluso después de cambiar el tamaño de mis imágenes a los tamaños correctos. Después de quitar mi código a lo esencial, finalmente descubrí que UIImage # imageWithContentsOfFile era el culpable. Aunque estaba precalentando un caching de UIImages utilizando este método, UIImage parece hacer algún tipo de caching perezoso internamente. Después de actualizar mi código de calentamiento del caching para usar la solución detallada aquí – stackoverflow.com/a/10664707/71810 – finalmente tuve un desplazamiento súper suave ~ 60FPS.

Creo que el enfoque # 3 es el mejor path a seguir y creo haber detectado el error.

Está asignando a @image , una variable privada para la class de vista de colección completa, en su bloque de operaciones. Probablemente debería cambiar:

 @image = UIImage.imageWithContentsOfFile(image_path) 

a

 image = UIImage.imageWithContentsOfFile(image_path) 

Y cambie todas las references de @image para usar la variable local. Estoy dispuesto a apostar que el problema es que cada vez que creas un bloque de operaciones y asignas a la variable de instancia, estás reemplazando lo que ya está allí. Debido a la aleatoriedad de cómo se bloquean los bloques de operación, la callback asíncrona de la queue principal recupera la misma image porque está accediendo a la última vez que se asignó @image .

En esencia, @image actúa como una variable global para la operación y bloques de callback asincrónica.

La mejor manera que encontré de resolver este problema fue manteniendo mi propio caching de imágenes, y precalentándolo en viewWillAppear en un hilo de background, como sigue:

 - (void) warmThubnailCache { if (self.isWarmingThumbCache) { return; } if ([NSThread isMainThread]) { // do it on a background thread [NSThread detachNewThreadSelector:@selector(warmThubnailCache) toTarget:self withObject:nil]; } else { self.isWarmingThumbCache = YES; NIImageMemoryCache *thumbCache = self.thumbnailImageCache; for (GalleryImage *galleryImage in _galleryImages) { NSString *cacheKey = galleryImage.thumbImageName; UIImage *thumbnail = [thumbCache objectWithName:cacheKey]; if (thumbnail == nil) { // populate cache thumbnail = [UIImage imageWithContentsOfFile:galleryImage.thumbImageName]; [thumbCache storeObject:thumbnail withName:cacheKey]; } } self.isWarmingThumbCache = NO; } } 

También puede usar GCD en lugar de NSThread, pero opté por NSThread, ya que solo uno de ellos debería ejecutarse a la vez, por lo que no es necesario hacer queue. Además, si tiene una cantidad esencialmente ilimitada de imágenes, tendrá que ser más cuidadoso que yo. Deberá ser más listo para cargar las imágenes alnetworkingedor de la location de desplazamiento de un usuario, en lugar de todas, como lo hago aquí. Puedo hacer esto porque la cantidad de imágenes, para mí, está arreglada.

También debería agregar que su colecciónView: cellForItemAtIndexPath: debería usar su caching, así:

 - (PSUICollectionViewCell *)collectionView:(PSUICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"GalleryCell"; GalleryCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath]; GalleryImage *galleryImage = [_galleryManager.galleryImages objectAtIndex:indexPath.row]; // use cached images first NIImageMemoryCache *thumbCache = self.galleryManager.thumbnailImageCache; NSString *cacheKey = galleryImage.thumbImageName; UIImage *thumbnail = [thumbCache objectWithName:cacheKey]; if (thumbnail != nil) { cell.imageView.image = thumbnail; } else { UIImage *image = [UIImage imageWithContentsOfFile:galleryImage.thumbImageName]; cell.imageView.image = image; // store image in cache [thumbCache storeObject:image withName:cacheKey]; } return cell; } 

Asegúrese de que su memory caching se borre cuando reciba una advertencia de memory, o use algo como NSCache. Estoy usando un marco llamado Nimbus, que tiene algo llamado NIImageMemoryCache, que me permite establecer un número máximo de píxeles en el caching. Muy práctico

Aparte de eso, uno de los puntos a tener en count es NO usar el método "imageNamed" de UIImage, que es su propio almacenamiento en caching, y le dará problemas de memory. Use "imageWithContentsOfFile" en su lugar.