objective c - La forma correcta de tratar con la reutilización de células con hilos de fondo?
objective-c multithreading (3)
Tengo un UICollectionView
, pero los mismos métodos deben aplicarse a UITableViews
. Cada una de mis celdas contiene una imagen que cargo desde el disco, que es una operación lenta. Para mitigar esto, uso una cola de envío asincrónico. Esto funciona bien, pero los resultados de desplazamiento rápido en estas operaciones se acumulan, de modo que la celda cambiará su imagen de uno a otro en secuencia, hasta que finalmente se detiene en la última llamada.
En el pasado, hice un control para ver si la celda todavía estaba visible, y si no, no continúo. Sin embargo, esto no funciona con UICollectionView y, de todos modos, no es eficiente. Estoy considerando migrar para usar NSOperations
, que se puede cancelar, de modo que solo se NSOperations
la última llamada para modificar la celda. Posiblemente podría hacer esto comprobando si la operación había finalizado en el método prepareForReuse
y cancelando si no. Espero que alguien haya solucionado este problema en el pasado y pueda brindar algunos consejos o una solución.
¿Has intentado utilizar el marco de código abierto SDWebImage que funciona con la descarga de imágenes de la web y el almacenamiento en caché de forma asincrónica? Funciona de maravilla con UITableViews
y debería ser tan bueno con UICollectionViews
. Simplemente usa el setImage:withURL:
de la extensión UIImageView
y hace magia.
Tuve el mismo problema con una UITableView de alrededor de 400 elementos y encontré una solución brutal pero funcional: ¡no reutilice las células! Depende de cuánto y cuán grandes sean las imágenes, pero en realidad iOS es bastante eficiente en la desasignación de celdas no utilizadas ... De nuevo: no es lo que sugerirá el purista, pero la pareja con carga asíncrona se ilumina rápidamente, no tiene errores y no tiene que lidiar con la lógica de sincronización compleja.
La sesión 211, Creación de interfaces de usuario simultáneas en iOS, de la WWDC 2012 , analiza el problema de la imagen cambiante de la celda a medida que las tareas en segundo plano se ponen al día (comenzando en 38m15s).
Así es como resuelves ese problema. Después de cargar la imagen en la cola de fondo, use la ruta de índice para buscar la celda actual, si la hay, que muestra el elemento que contiene esa imagen. Si obtienes una celda, configura la imagen de la celda. Si obtienes nulo, no hay una celda que muestre ese elemento así que simplemente descarta la imagen.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
cell.imageView.image = nil;
dispatch_async(myQueue, ^{
[self bg_loadImageForRowAtIndexPath:indexPath];
});
return cell;
}
- (void)bg_loadImageForRowAtIndexPath:(NSIndexPath *)indexPath {
// I assume you have a method that returns the path of the image file. That
// method must be safe to run on the background queue.
NSString *path = [self bg_pathOfImageForItemAtIndexPath:indexPath];
UIImage *image = [UIImage imageWithContentsOfFile:path];
// Make sure the image actually loads its data now.
[image CGImage];
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:image forItemAtIndexPath:indexPath];
});
}
- (void)setImage:(UIImage *)image forItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = (MyCell *)[collectionView cellForItemAtIndexPath:indexPath];
// If cell is nil, the following line has no effect.
cell.imageView.image = image;
}
Aquí hay una manera fácil de reducir el "apilamiento" de tareas en segundo plano al cargar imágenes que la vista ya no necesita. NSMutableSet
una variable de instancia NSMutableSet
:
@implementation MyViewController {
dispatch_queue_t myQueue;
NSMutableSet *indexPathsNeedingImages;
}
Cuando necesite cargar una imagen, coloque la ruta de índice de la celda en el conjunto y envíe una tarea que quite una ruta de índice del conjunto y cargue su imagen:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
cell.imageView.image = nil;
[self addIndexPathToImageLoadingQueue:indexPath];
return cell;
}
- (void)addIndexPathToImageLoadingQueue:(NSIndexPath *)indexPath {
if (!indexPathsNeedingImages) {
indexPathsNeedingImages = [NSMutableSet set];
}
[indexPathsNeedingImages addObject:indexPath];
dispatch_async(myQueue, ^{ [self bg_loadOneImage]; });
}
Si una celda deja de mostrarse, elimine la ruta de índice de la celda del conjunto:
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
[indexPathsNeedingImages removeObject:indexPath];
}
Para cargar una imagen, obtenga una ruta de índice del conjunto. Tenemos que enviar de vuelta a la cola principal para esto, para evitar una condición de carrera en el conjunto:
- (void)bg_loadOneImage {
__block NSIndexPath *indexPath;
dispatch_sync(dispatch_get_main_queue(), ^{
indexPath = [indexPathsNeedingImages anyObject];
if (indexPath) {
[indexPathsNeedingImages removeObject:indexPath];
}
}
if (indexPath) {
[self bg_loadImageForRowAtIndexPath:indexPath];
}
}
El método bg_loadImageForRowAtIndexPath:
es el mismo que el anterior. Pero cuando realmente establecemos la imagen en la celda, también queremos eliminar la ruta de índice de la celda del conjunto:
- (void)setImage:(UIImage *)image forItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = (MyCell *)[collectionView cellForItemAtIndexPath:indexPath];
// If cell is nil, the following line has no effect.
cell.imageView.image = image;
[indexPathsNeedingImages removeObject:indexPath];
}