[iphone] Ultimo stack In-First Out con GCD?


3 Answers

Il codice seguente crea un ultimo stack in-first out elaborato in background utilizzando Grand Central Dispatch. La classe SYNStackController è generica e riutilizzabile, ma in questo esempio viene fornito anche il codice per il caso d'uso identificato nella domanda, il rendering delle immagini della cella della tabella in modo asincrono e la garanzia che quando si interrompe lo scorrimento rapido, le celle attualmente visualizzate siano le prossime ad essere aggiornate.

Complimenti a Ben M. la cui risposta a questa domanda ha fornito il codice iniziale su cui questo era basato. (La sua risposta fornisce anche il codice che è possibile utilizzare per testare lo stack.) L'implementazione fornita qui non richiede ARC, e usa solamente Grand Central Dispatch piuttosto che performSelectorInBackground. Il codice seguente memorizza anche un riferimento alla cella corrente usando objc_setAssociatedObject che abiliterà l'immagine renderizzata ad essere associata alla cella corretta, quando l'immagine viene successivamente caricata in modo asincrono. Senza questo codice, le immagini renderizzate per i contatti precedenti verranno erroneamente inserite nelle celle riutilizzate anche se ora stanno visualizzando un contatto diverso.

Ho assegnato la generosità a Ben M. ma sto dando la risposta come risposta accettata dal momento che questo codice è stato completamente elaborato.

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

Nel view.h, prima di @interface:

@class SYNStackController;

Nella sezione view.h @interface:

SYNStackController *stackController;

Nella vista.h, dopo la sezione @interface:

@property (nonatomic, retain) SYNStackController *stackController;

Nel view.m, prima di @implementation:

#import "SYNStackController.h"

Nel view.m viewDidLoad:

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

Nel view.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;
}
Question

Ho un UITableView che visualizza le immagini associate ai contatti in ogni riga. In alcuni casi queste immagini vengono lette sul primo display dall'immagine di contatto della rubrica e, dove non ce n'è una, sono rappresentate da un avatar basato su dati memorizzati. Attualmente queste immagini vengono aggiornate su un thread in background utilizzando GCD. Tuttavia, carica le immagini nell'ordine in cui sono state richieste, il che significa che durante lo scorrimento rapido la coda diventa lunga e quando l'utente interrompe lo scorrimento le celle correnti sono le ultime ad essere aggiornate. Sull'iPhone 4, il problema non è molto evidente, ma sono desideroso di supportare hardware obsoleto e sto testando su un iPhone 3G. Il ritardo è tollerabile ma abbastanza evidente.

Mi sembra che uno stack Last In-First Out sembri probabile che risolva in gran parte questo problema, poiché ogni volta che l'utente interrompe lo scorrimento di quelle celle sarebbe il prossimo ad essere aggiornato e quindi gli altri attualmente fuori campo verrebbero aggiornati. Una cosa del genere è possibile con Grand Central Dispatch? O non troppo oneroso per implementare un altro modo?

Nota, a proposito, che sto usando Core Data con un negozio SQLite e non sto usando un NSFetchedResultsController a causa di una relazione molti-a-molti che deve essere attraversata per caricare i dati per questa vista. (Per quanto ne so, questo preclude l'utilizzo di un NSFetchedResultsController.) [Ho scoperto che un NSFetchedResultsController può essere utilizzato con relazioni many-to-many, nonostante ciò che la documentazione ufficiale sembra dire. Ma non sto ancora usando uno in questo contesto.]

Aggiunta: basti notare che mentre l'argomento è "Come creo un ultimo attacco con il GCD", in realtà voglio solo risolvere il problema descritto sopra e potrebbe esserci un modo migliore per farlo. Sono più che aperto a suggerimenti come quello di timthetoolman che risolve il problema delineato in un altro modo; se un tale suggerimento è finalmente quello che uso, riconoscerò sia la migliore risposta alla domanda originale sia la soluzione migliore che ho finito per implementare ... :)




Faccio qualcosa di simile, ma solo per iPad, e sembra abbastanza veloce. NSOperationQueue (o GCD non NSOperationQueue ) sembra l'approccio più semplice, in quanto tutto può essere autonomo e non è necessario preoccuparsi della sincronizzazione. Inoltre, potresti essere in grado di salvare l'ultima operazione e usare setQueuePriority: per abbassarlo. Quindi il più recente verrà estratto dalla coda per primo. O passare attraverso tutte le -operations in coda e abbassare la loro priorità. (Probabilmente potresti farlo dopo aver completato ognuno di essi, presumo che questo sarebbe ancora significativamente più veloce di fare il lavoro stesso.)




Un metodo semplice che può essere abbastanza buono per il tuo compito: usa la NSOperation delle dipendenze di NSOperation .

Quando è necessario inviare un'operazione, ottenere le operazioni della coda e cercare l'ultima inviata (ad es. Ricerca indietro alla fine dell'array) che non è ancora stata avviata. Se esiste, addDependency: base alla nuova operazione con addDependency: Quindi aggiungi la tua nuova operazione.

Questo costruisce una catena di dipendenze inversa attraverso le operazioni non avviate che le obbligheranno a eseguire in serie, last-in-first-out, come disponibili. Se si desidera consentire l'esecuzione simultanea di n (> 1) operazioni: trovare la più recente operazione non avviata aggiunta e aggiungere la dipendenza ad essa. (e naturalmente impostare il maxConcurrentOperationCount della coda su n .) Ci sono casi limite in cui questo non sarà 100% LIFO, ma dovrebbe essere abbastanza buono per il jazz.

(Questo non copre la ridefinizione delle priorità delle operazioni se (ad esempio) un utente scorre verso il basso l'elenco e quindi esegue il backup di un bit, tutto più velocemente di quanto la coda possa riempire le immagini. Se vuoi affrontare questo caso e ti sei dato un modo per localizzare l'operazione corrispondente già accodata ma non avviata, è possibile cancellare le dipendenze da tale operazione, riportandola in modo efficace al "capo della linea", ma dal momento che è il primo ad essere eliminato è già abbastanza buono, potresti non aver bisogno di avere questa fantasia.)

[modificato per aggiungere:]

Ho implementato qualcosa di molto simile a questo: un tavolo di utenti, i loro avatar pigri recuperati da gravatar.com sullo sfondo - e questo trucco ha funzionato alla grande. Il codice precedente era:

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

che divenne:

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];

Le icone appaiono visibilmente dall'alto verso il basso nel primo caso. Nel secondo, il primo carica, poi il resto carica dal basso verso l'alto; e scorrendo rapidamente verso il basso si verifica un caricamento occasionale, quindi il caricamento immediato (dalla parte inferiore) delle icone della schermata in cui ci si ferma. Molto appiccicoso, molto più "divertente" per l'app.




Sono un grande fan dell'interfaccia di NSOperationQueue e della sua facilità d'uso, ma avevo anche bisogno di una versione LIFO. Ho finito per implementare una versione LIFO di NSOperationQueue che ha NSOperationQueue abbastanza bene per me. NSOperationQueue l'interfaccia di NSOperationQueue , ma esegue le cose in un ordine (approssimativo) LIFO.




Related