objective c - Pobre rendimiento de desplazamiento UICollectionView con UIImage
objective-c performance (3)
Este es el patrón que sigo. Siempre cargue asynch y guarde en caché el resultado. No haga suposiciones sobre el estado de la vista cuando finaliza la carga de sincronización. Tengo una clase que simplifica las cargas de la siguiente manera:
//
// ImageRequest.h
// This class keeps track of in-flight instances, creating only one NSURLConnection for
// multiple matching requests (requests with matching URLs). It also uses NSCache to cache
// retrieved images. Set the cache count limit with the macro in this file.
#define kIMAGE_REQUEST_CACHE_LIMIT 100
typedef void (^CompletionBlock) (UIImage *, NSError *);
@interface ImageRequest : NSMutableURLRequest
- (UIImage *)cachedResult;
- (void)startWithCompletion:(CompletionBlock)completion;
@end
//
// ImageRequest.m
#import "ImageRequest.h"
NSMutableDictionary *_inflight;
NSCache *_imageCache;
@implementation ImageRequest
- (NSMutableDictionary *)inflight {
if (!_inflight) {
_inflight = [NSMutableDictionary dictionary];
}
return _inflight;
}
- (NSCache *)imageCache {
if (!_imageCache) {
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
}
return _imageCache;
}
- (UIImage *)cachedResult {
return [self.imageCache objectForKey:self];
}
- (void)startWithCompletion:(CompletionBlock)completion {
UIImage *image = [self cachedResult];
if (image) return completion(image, nil);
NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
if (inflightCompletionBlocks) {
// a matching request is in flight, keep the completion block to run when we''re finished
[inflightCompletionBlocks addObject:completion];
} else {
[self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// build an image, cache the result and run completion blocks for this request
UIImage *image = [UIImage imageWithData:data];
[self.imageCache setObject:image forKey:self];
id value = [self.inflight objectForKey:self];
[self.inflight removeObjectForKey:self];
for (CompletionBlock block in (NSMutableArray *)value) {
block(image, nil);
}
} else {
[self.inflight removeObjectForKey:self];
completion(nil, error);
}
}];
}
}
@end
Ahora la actualización de la celda (colección o tabla) es bastante simple:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
NSURL *url = [NSURL URLWithString:@"http:// some url from your model"];
// note that this can be a web url or file url
ImageRequest *request = [[ImageRequest alloc] initWithURL:url];
UIImage *image = [request cachedResult];
if (image) {
UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
imageView.image = image;
} else {
[request startWithCompletion:^(UIImage *image, NSError *error) {
if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
[collectionView reloadItemsAtIndexPaths:@[indexPath]];
}
}];
}
return cell;
}
Tengo un UICollectionView en mi aplicación, y cada celda es un UIImageView y algunas etiquetas de texto. El problema es que cuando tengo UIImageViews mostrando sus imágenes, el rendimiento de desplazamiento es terrible. No es tan fácil como la experiencia de desplazamiento de un UITableView o incluso el mismo UICollectionView sin el UIImageView.
Encontré esta pregunta hace unos meses y parece que se encontró una respuesta, pero está escrita en RubyMotion, y no lo entiendo. Traté de ver cómo convertirlo a Xcode, pero dado que nunca he usado NSCache tampoco, es un poco difícil. El cartel también señaló aquí sobre la implementación de algo además de su solución, pero tampoco estoy seguro de dónde poner ese código. Posiblemente porque no entiendo el código de la primera pregunta .
¿Alguien podría ayudar a traducir esto en Xcode?
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
Aquí está el código de la segunda pregunta que el autor de la primera pregunta dijo que resolvió el problema:
UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];
CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
De nuevo, no estoy seguro de cómo / dónde implementar eso.
Muchas gracias.
Tuve problemas con el desplazamiento de UICollectionView
.
Lo que funcionó (casi) como un encanto para mí: llené las celdas con miniaturas PNG 90x90. Digo casi porque el primer rollo completo no es tan suave, pero nunca se colgó.
En mi caso, el tamaño de la celda es 90x90.
Tuve muchos tamaños png originales antes, y fue muy entrecortado cuando png el tamaño original fue mayor que ~ 1000x1000 (muchos bloqueos en el primer desplazamiento).
Entonces, selecciono 90x90 (o similar) en UICollectionView
y visualizo los png''s originales (sin importar el tamaño). Espero que pueda ayudar a otros.
En general, el comportamiento de desplazamiento incorrecto para UICollectionViews o UITableViews ocurre porque las celdas son quitadas de la cola y construidas en el hilo principal por iOS. Hay poca libertad para precachear celdas o construirlas en un subproceso en segundo plano, sino que se quitan de la cola y se construyen a medida que desplaza el bloqueo de la interfaz de usuario. (Personalmente, este diseño de Apple me parece malo aunque simplifica las cosas porque no tienes que estar al tanto de posibles problemas de subprocesos. Creo que deberían haber dado un gancho para proporcionar una implementación personalizada para un grupo de UICollectionViewCell / UITableViewCell que puede manejar la eliminación / reutilización de celdas).
Las causas más importantes para la disminución del rendimiento están de hecho relacionadas con los datos de imagen y (en orden decreciente de magnitud) según mi experiencia:
- Llamadas sincrónicas para descargar datos de imagen: siempre haga esto de forma asincrónica y llame a [UIImageView setImage:] con la imagen construida cuando esté listo en el hilo principal
- Llamadas sincrónicas para construir imágenes a partir de datos en el sistema de archivos local, o de otros datos serializados: hágalo también de forma asíncrona. (por ejemplo [UIImage imageWithContentsOfFile:], [UIImage imageWithData:], etc.).
- Llamadas a [UIImage imageNamed:]: la primera vez que se carga esta imagen, se envía desde el sistema de archivos. Es posible que desee precachear imágenes (simplemente cargando [UIImage imageNamed:] antes de que la celda esté realmente construida, de modo que puedan ser servidas desde la memoria de inmediato.
- Llamar a [UIImageView setImage:] tampoco es el método más rápido, pero a menudo no se puede evitar a menos que use imágenes estáticas. En el caso de las imágenes estáticas, a veces es más rápido utilizar diferentes vistas de imágenes que configura como ocultas o no, según si deben mostrarse en lugar de cambiar la imagen en la misma vista de imagen.
- La primera vez que se extrae la llamada de una celda, se carga desde un Nib o se construye con alloc-init y se establecen algunos diseños o propiedades iniciales (probablemente también imágenes si los usó). Esto causa un mal comportamiento de desplazamiento la primera vez que se utiliza una celda.
Debido a que soy muy exigente con el desplazamiento suave (incluso si es la primera vez que se usa una celda), construí un marco completo para precachear las células mediante la subclasificación de UINib (este es básicamente el único enganche al proceso de extracción utilizado por iOS). Pero eso puede estar más allá de tus necesidades.