¿Última stack de entradas y salidas con GCD?

Tengo una UITableView que muestra imágenes asociadas con contactos en cada fila. En algunos casos, estas imágenes se leen la primera vez que se visualizan desde la imagen de contacto de la libreta de direcciones, y donde no hay ninguna, se representan como un avatar en función de los datos almacenados. Actualmente tengo estas imágenes actualizadas en un hilo de fondo usando GCD. Sin embargo, esto carga las imágenes en el orden en que fueron solicitadas, lo que significa que durante el desplazamiento rápido la cola se vuelve larga y cuando el usuario deja de desplazarse, las celdas actuales son las últimas en actualizarse. En el iPhone 4, el problema no es realmente notable, pero estoy dispuesto a admitir hardware antiguo y estoy probando en un iPhone 3G. La demora es tolerable pero bastante notable.

Me parece que una stack de Último entrado primero podría resolver en gran medida este problema, ya que cada vez que el usuario dejaba de desplazarse, esas celdas serían las siguientes en actualizarse y luego las otras que actualmente están fuera de pantalla se actualizarían. ¿Es esto posible con Grand Central Dispatch? ¿O no demasiado oneroso para implementar de otra manera?

Tenga en cuenta, por cierto, que estoy utilizando Core Data con una tienda SQLite y no estoy usando un NSFetchedResultsController debido a una relación de muchos a muchos que debe atravesarse para cargar los datos de esta vista. (Hasta donde yo sé, eso imposibilita el uso de NSFetchedResultsController). [Descubrí que un NSFetchedResultsController se puede usar con relaciones de muchos a muchos, a pesar de lo que parece indicar la documentación oficial. Pero no estoy usando uno en este contexto, todavía.]

Además: Solo para señalar que, si bien el tema es “¿Cómo puedo crear una última stack de entradas y salidas con GCD?”, En realidad solo quiero resolver el problema descrito anteriormente y es posible que haya una mejor manera de hacerlo. Estoy más que dispuesto a sugerencias como la de Timthetool que resuelve el problema planteado de otra manera; si tal sugerencia es finalmente lo que uso, reconoceré tanto la mejor respuesta a la pregunta original como la mejor solución que terminé implementando … 🙂

Debido a las limitaciones de memoria del dispositivo, debe cargar las imágenes a pedido y en una cola GCD de fondo. En el método cellForRowAtIndexPath: compruebe si la imagen de su contacto es nula o ha sido almacenada en caché. Si la imagen es nula o no está en caché, use un dispatch_async nested para cargar la imagen de la base de datos y actualizar la celda tableView.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // If the contact object's image has not been loaded, // Use a place holder image, then use dispatch_async on a background queue to retrieve it. if (contact.image!=nil){ [[cell imageView] setImage: contact.image]; }else{ // Set a temporary placeholder [[cell imageView] setImage: placeHolderImage]; // Retrieve the image from the database on a background queue dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_async(queue, ^{ UIImage *image = // render image; contact.image=image; // use an index path to get at the cell we want to use because // the original may be reused by the OS. UITableViewCell *theCell=[tableView cellForRowAtIndexPath:indexPath]; // check to see if the cell is visible if ([tableView visibleCells] containsObject: theCell]){ // put the image into the cell's imageView on the main queue dispatch_async(dispatch_get_main_queue(), ^{ [[theCell imageView] setImage:contact.image]; [theCell setNeedsLayout]; }); } }); } return cell; } 

El video de la conferencia WWDC2010 “Introducción a los bloques y al despacho de Grand Central” muestra un ejemplo que también usa el dispatch_async nested.

Otra posible optimización podría ser comenzar a descargar las imágenes en una cola de segundo plano de baja prioridad cuando se inicie la aplicación. es decir

  // in the ApplicationDidFinishLaunchingWithOptions method // dispatch in on the main queue to get it working as soon // as the main queue comes "online". A trick mentioned by // Apple at WWDC dispatch_async(dispatch_get_main_queue(), ^{ // dispatch to background priority queue as soon as we // get onto the main queue so as not to block the main // queue and therefore the UI dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) dispatch_apply(contactsCount,lowPriorityQueue ,^(size_t idx){ // skip the first 25 because they will be called // almost immediately by the tableView if (idx>24){ UIImage *renderedImage =/// render image [[contactsArray objectAtIndex: idx] setImage: renderedImage]; } }); }); 

Con este despacho nested, estamos renderizando las imágenes en una cola de prioridad extremadamente baja. Al colocar la representación de la imagen en la cola de prioridad de fondo, las imágenes que se están representando desde el método de celdaForRowAtIndexPath se mostrarán con mayor prioridad. Entonces, debido a la diferencia en las prioridades de las colas, tendrá un LIFO “pobre”.

Buena suerte.

El siguiente código crea una stack flexible de última entrada primero que se procesa en segundo plano usando Grand Central Dispatch. La clase SYNStackController es genérica y reutilizable, pero este ejemplo también proporciona el código para el caso de uso identificado en la pregunta, representa imágenes de celda de tabla de forma asincrónica y garantiza que cuando se detiene el desplazamiento rápido, las celdas mostradas actualmente son las siguientes en actualizarse.

Felicitaciones a Ben M. cuya respuesta a esta pregunta proporcionó el código inicial en el que se basó. (Su respuesta también proporciona un código que puede usar para probar la stack.) La implementación que se proporciona aquí no requiere ARC, y utiliza únicamente Grand Central Dispatch en lugar de performSelectorInBackground. El código a continuación también almacena una referencia a la celda actual usando objc_setAssociatedObject que permitirá que la imagen renderizada se asocie con la celda correcta, cuando la imagen se cargue posteriormente de forma asíncrona. Sin este código, las imágenes renderizadas para los contactos anteriores se insertarán incorrectamente en las celdas reutilizadas aunque ahora muestren un contacto diferente.

Le he otorgado la recompensa a Ben M. pero estoy marcando esto como la respuesta aceptada, ya que este código se ha trabajado más a fondo.

SYNStackController.h

 // // SYNStackController.h // Last-in-first-out stack controller class. // @interface SYNStackController : NSObject { NSMutableArray *stack; } - (void) addBlock:(void (^)())block; - (void) startNextBlock; + (void) performBlock:(void (^)())block; @end 

SYNStackController.m

 // // SYNStackController.m // Last-in-first-out stack controller class. // #import "SYNStackController.h" @implementation SYNStackController - (id)init { self = [super init]; if (self != nil) { stack = [[NSMutableArray alloc] init]; } return self; } - (void)addBlock:(void (^)())block { @synchronized(stack) { [stack addObject:[[block copy] autorelease]]; } if (stack.count == 1) { // If the stack was empty before this block was added, processing has ceased, so start processing. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^{ [self startNextBlock]; }); } } - (void)startNextBlock { if (stack.count > 0) { @synchronized(stack) { id blockToPerform = [stack lastObject]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^{ [SYNStackController performBlock:[[blockToPerform copy] autorelease]]; }); [stack removeObject:blockToPerform]; } [self startNextBlock]; } } + (void)performBlock:(void (^)())block { @autoreleasepool { block(); } } - (void)dealloc { [stack release]; [super dealloc]; } @end 

En la view.h, antes de @interface:

 @class SYNStackController; 

En la sección view.h @interface:

 SYNStackController *stackController; 

En la vista.h, después de la sección @interface:

 @property (nonatomic, retain) SYNStackController *stackController; 

En la view.m, antes de @implementation:

 #import "SYNStackController.h" 

En view.idLoad de view.m:

 // Initialise Stack Controller. self.stackController = [[[SYNStackController alloc] init] autorelease]; 

En la vista.m:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // Set up the cell. static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } else { // If an existing cell is being reused, reset the image to the default until it is populated. // Without this code, previous images are displayed against the new people during rapid scrolling. [cell setImage:[UIImage imageNamed:@"DefaultPicture.jpg"]]; } // Set up other aspects of the cell content. ... // Store a reference to the current cell that will enable the image to be associated with the correct // cell, when the image subsequently loaded asynchronously. objc_setAssociatedObject(cell, personIndexPathAssociationKey, indexPath, OBJC_ASSOCIATION_RETAIN); // Queue a block that obtains/creates the image and then loads it into the cell. // The code block will be run asynchronously in a last-in-first-out queue, so that when // rapid scrolling finishes, the current cells being displayed will be the next to be updated. [self.stackController addBlock:^{ UIImage *avatarImage = [self createAvatar]; // The code to achieve this is not implemented in this example. // The block will be processed on a background Grand Central Dispatch queue. // Therefore, ensure that this code that updates the UI will run on the main queue. dispatch_async(dispatch_get_main_queue(), ^{ NSIndexPath *cellIndexPath = (NSIndexPath *)objc_getAssociatedObject(cell, personIndexPathAssociationKey); if ([indexPath isEqual:cellIndexPath]) { // Only set cell image if the cell currently being displayed is the one that actually required this image. // Prevents reused cells from receiving images back from rendering that were requested for that cell in a previous life. [cell setImage:avatarImage]; } }); }]; return cell; } 

Ok, lo he probado y funciona. El objeto simplemente saca el siguiente bloque de la stack y lo ejecuta de forma asincrónica. En la actualidad, solo funciona con bloques de retorno vacíos, pero podría hacer algo como agregar un objeto que tenga un bloque y un delegado para pasar el tipo de retorno del bloque.

NOTA: Utilicé ARC en esto, así que necesitarás el XCode 4.2 o superior, para aquellos de ti en versiones posteriores, simplemente cambia el fuerte para retener y estarás bien, pero la memoria perderá todo si no agregas en lanzamientos.

EDITAR: Para ser más específico en su caso de uso, si su TableViewCell tiene una imagen, usaría mi clase de stack de la siguiente manera para obtener el rendimiento que desea, por favor avíseme si funciona bien para usted.

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... UIImage *avatar = [self getAvatarIfItExists]; // I you have a method to check for the avatar if (!avatar) { [self.blockStack addBlock:^{ // do the heavy lifting with your creation logic UIImage *avatarImage = [self createAvatar]; dispatch_async(dispatch_get_main_queue(), ^{ //return the created image to the main thread. cell.avatarImageView.image = avatarImage; }); }]; } else { cell.avatarImageView.image = avatar; } return cell; } 

Aquí está el código de prueba que muestra que funciona como una stack:

 WaschyBlockStack *stack = [[WaschyBlockStack alloc] init]; for (int i = 0; i < 100; i ++) { [stack addBlock:^{ NSLog(@"Block operation %i", i); sleep(1); }]; } 

Aquí está el .h:

 #import  @interface WaschyBlockStack : NSObject { NSMutableArray *_blockStackArray; id _currentBlock; } - (id)init; - (void)addBlock:(void (^)())block; @end 

Y ellos:

 #import "WaschyBlockStack.h" @interface WaschyBlockStack() @property (atomic, strong) NSMutableArray *blockStackArray; - (void)startNextBlock; + (void)performBlock:(void (^)())block; @end @implementation WaschyBlockStack @synthesize blockStackArray = _blockStackArray; - (id)init { self = [super init]; if (self) { self.blockStackArray = [NSMutableArray array]; } return self; } - (void)addBlock:(void (^)())block { @synchronized(self.blockStackArray) { [self.blockStackArray addObject:block]; } if (self.blockStackArray.count == 1) { [self startNextBlock]; } } - (void)startNextBlock { if (self.blockStackArray.count > 0) { @synchronized(self.blockStackArray) { id blockToPerform = [self.blockStackArray lastObject]; [WaschyBlockStack performSelectorInBackground:@selector(performBlock:) withObject:[blockToPerform copy]]; [self.blockStackArray removeObject:blockToPerform]; } [self startNextBlock]; } } + (void)performBlock:(void (^)())block { block(); } @end 

Un método simple que puede ser lo suficientemente bueno para su tarea: use la función de dependencias de NSOperation s ‘.

Cuando necesite enviar una operación, obtenga las operaciones de la cola y busque la última enviada (es decir, búsqueda desde el final de la matriz) que aún no se haya iniciado. Si tal existe, addDependency: para que dependa de su nueva operación con addDependency: Luego agrega tu nueva operación.

Esto crea una cadena de dependencia inversa a través de las operaciones no iniciadas que les obligará a ejecutar en serie, último en entrar, primero en salir, según esté disponible. Si desea permitir que n (> 1) operaciones se ejecuten simultáneamente: encuentre la enésima operación no iniciada agregada recientemente y agregue la dependencia a ella. (y, por supuesto, establezca maxConcurrentOperationCount la cola en n .) Hay casos maxConcurrentOperationCount en los que esto no será 100% LIFO, pero debería ser lo suficientemente bueno para el jazz.

(Esto no cubre las operaciones de re-priorización si (por ejemplo) un usuario se desplaza hacia abajo de la lista y luego retrocede un poco, todo más rápido de lo que la cola puede completar las imágenes. Si desea abordar este caso, y se ha dado a sí mismo una forma de localizar la operación correspondiente ya en cola pero no iniciada, puede borrar las dependencias de esa operación. Esto efectivamente lo regresa al “principio de la línea”. Pero desde el primero en entrar, primero en salir ya es casi lo suficientemente bueno, puede que no necesites esta fantasía.)

[editado para agregar:]

He implementado algo muy parecido a esto – una tabla de usuarios, sus avatares extraídos de gravatar.com en segundo plano – y este truco funcionó muy bien. El código anterior era:

 [avatarQueue addOperationWithBlock:^{ // slow code }]; // avatarQueue is limited to 1 concurrent op 

el cual se convirtió:

 NSBlockOperation *fetch = [NSBlockOperation blockOperationWithBlock:^{ // same slow code }]; NSArray *pendingOps = [avatarQueue operations]; for (int i = pendingOps.count - 1; i >= 0; i--) { NSOperation *op = [pendingOps objectAtIndex:i]; if (![op isExecuting]) { [op addDependency:fetch]; break; } } [avatarQueue addOperation:fetch]; 

Los íconos aparecen visiblemente de arriba hacia abajo en el primer caso. En el segundo, el superior se carga, luego el rest se carga de abajo hacia arriba; y al desplazarse rápidamente hacia abajo ocasiona una carga ocasional, luego carga inmediata (desde la parte inferior) de los íconos de la pantalla en la que se detiene. Muy pulido, mucho más “ágil” para la aplicación.

No lo he intentado, solo estoy lanzando ideas.

Podrías mantener tu propia stack. Agregue a la stack y haga cola a GCD en el hilo de primer plano. El bloque de código que hace cola a GCD simplemente saca el siguiente bloque de la stack (la stack misma necesitaría sincronización interna para push & pop) y lo ejecuta.

Otra opción puede ser simplemente omitir el trabajo si hay más de n elementos en la cola. Eso significaría que si rápidamente hiciera una copia de seguridad de la cola, se presionaría rápidamente en la cola y solo procesaría

Espero que haya despertado algunas ideas … Es posible que lo use más adelante en el código.

Hago algo como esto, pero solo para iPad, y me pareció lo suficientemente rápido. NSOperationQueue (o GCD en bruto) parece ser el enfoque más simple, ya que todo puede ser autónomo y no es necesario preocuparse por la sincronización. Además, es posible que pueda guardar la última operación y usar setQueuePriority: para disminuirla. Luego, el más reciente será retirado de la cola primero. O -operations todas las -operations en la cola y disminuya su prioridad. (Probablemente podría hacer esto después de completar cada uno, supongo que esto sería mucho más rápido que hacer el trabajo mismo).

crea una stack segura para hilos, usando algo como esto como punto de partida:

 @interface MONStack : NSObject  // << expose object's lock so you // can easily perform many pushes // at once, keeping everything current. { @private NSMutableArray * objects; NSRecursiveLock * lock; } /** @brief pushes @a object onto the stack. if you have to do many pushes at once, consider adding `addObjects:(NSArray *)` */ - (void)addObject:(id)object; /** @brief removes and returns the top object from the stack */ - (id)popTopObject; /** @return YES if the stack contains zero objects. */ - (BOOL)isEmpty; @end @implementation MONStack - (id)init { self = [super init]; if (0 != self) { objects = [NSMutableArray new]; lock = [NSRecursiveLock new]; if (0 == objects || 0 == lock) { [self release]; return 0; } } return self; } - (void)lock { [lock lock]; } - (void)unlock { [lock unlock]; } - (void)dealloc { [lock release], lock = 0; [objects release], objects = 0; [super dealloc]; } - (void)addObject:(id)object { [self lock]; [objects addObject:object]; [self unlock]; } - (id)popTopObject { [self lock]; id last = 0; if ([objects count]) { last = [[[objects lastObject] retain] autorelease]; } [self unlock]; return last; } - (BOOL)isEmpty { [self lock]; BOOL ret = 0 == [objects count]; [self unlock]; return ret; } @end 

luego use una subclase de NSOperation (o GCD, si lo prefiere). puedes compartir la stack entre la operación y los clientes.

así que el bit vacío y el principal de NSOperation son las secciones algo complicadas.

comencemos con el bit vacío esto es complicado porque debe ser seguro para los hilos:

 // adding a request and creating the operation if needed: { MONStack * stack = self.stack; [stack lock]; BOOL wasEmptyBeforePush = [stack isEmpty]; [stack addObject:thing]; if (wasEmptyBeforePush) { [self.operationQueue addOperation:[MONOperation operationWithStack:stack]]; } [stack unlock]; // ... } 

el principal de NSOperation debería pasar y agotar la stack, crear un grupo de autorrelease para cada tarea y verificar la cancelación. cuando la stack está vacía o la operación se cancela, limpie y salga principal. el cliente creará una nueva operación cuando sea necesario.

la cancelación de soporte para solicitudes más lentas (por ejemplo, red o disco) puede marcar una gran diferencia. la cancelación en el caso de la operación que agotó la cola requeriría que la vista solicitante pudiera eliminar su solicitud cuando se descargue (por ejemplo, para reutilizar durante el desplazamiento).

Otro error común: la carga asíncrona inmediata (por ejemplo, agregar la operación a la cola de operación) de la imagen puede degradar fácilmente el rendimiento. medida.

si la tarea se beneficia de la paralelización, permite múltiples tareas en la cola de operaciones.

también debe identificar las solicitudes redundantes (imagínese un usuario desplazándose bidireccionalmente) en su cola de tareas, si su progtwig es capaz de producirlas.

Soy un gran admirador de la interfaz y facilidad de uso de NSOperationQueue , pero también necesitaba una versión de LIFO. Terminé implementando una versión LIFO de NSOperationQueue aquí que se ha mantenido bastante bien para mí. NSOperationQueue la interfaz de NSOperationQueue , pero ejecuta cosas en un orden (aproximadamente) LIFO.