iphone - Ultimo stack In-First Out con GCD?




objective-c ios uitableview grand-central-dispatch (8)

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 ... :)


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

Ok, ho provato questo e funziona. L'oggetto estrae semplicemente il blocco successivo dallo stack ed esegue in modo asincrono. Attualmente funziona solo con i blocchi di restituzione vuoti, ma è possibile fare qualcosa di sofisticato come aggiungere un oggetto che avrà un blocco e un delegato per passare il tipo di ritorno del blocco a.

NOTA: Ho usato ARC in questo modo avrai bisogno di XCode 4.2 o versioni successive, per quelli di voi nelle versioni successive, basta cambiare il forte per mantenere e si dovrebbe andare bene, ma la memoria perde ogni cosa se non si aggiunge nelle versioni.

EDIT: per essere più specifico per il tuo caso d'uso, se il tuo TableViewCell ha un'immagine, userei la mia classe di stack nel modo seguente per ottenere le prestazioni che desideri, per favore fammi sapere se funziona bene per te.

- (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;
}

Ecco il codice di test che mostra che funziona come una pila:

WaschyBlockStack *stack = [[WaschyBlockStack alloc] init];

for (int i = 0; i < 100; i ++)
{
    [stack addBlock:^{

        NSLog(@"Block operation %i", i);

        sleep(1);

    }];
}

Ecco la .h:

#import <Foundation/Foundation.h>

@interface WaschyBlockStack : NSObject
{
    NSMutableArray *_blockStackArray;
    id _currentBlock;
}

- (id)init;
- (void)addBlock:(void (^)())block;

@end

E loro:

#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

A causa dei limiti di memoria del dispositivo, è necessario caricare le immagini su richiesta e su una coda GCD di sfondo. Nel cellaForRowAtIndexPath: metodo verifica se l'immagine del tuo contatto è nullo o è stata memorizzata nella cache. Se l'immagine è nul o no nella cache, utilizzare un dispatch_async nidificato per caricare l'immagine dal database e aggiornare la cella 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;
}

Il video della conferenza WWDC2010 "Introduzione a Blocchi e Grand Central Dispatch" mostra un esempio che utilizza anche il dispatch_async nidificato.

un'altra potenziale ottimizzazione potrebbe essere quella di iniziare a scaricare le immagini su una coda di sfondo a bassa priorità all'avvio dell'app. vale a dire

 // 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 questa spedizione annidata, stiamo rendendo le immagini su una coda a priorità estremamente bassa. Mettendo il rendering dell'immagine sulla coda priorità sfondo si consentirà di eseguire il rendering delle immagini dal metodo cellForRowAtIndexPath in precedenza a una priorità più alta. Quindi, a causa della differenza nelle priorità delle code, avrai un LIFO "poverino".

In bocca al lupo.


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.


Non ho provato questo - solo buttare idee là fuori.

Potresti mantenere il tuo stack. Aggiungi allo stack e accoda a GCD sul thread in primo piano. Il blocco di codice che accodate a GCD semplicemente estrae il blocco successivo dallo stack (lo stack stesso richiede la sincronizzazione interna per push & pop) e lo esegue.

Un'altra opzione potrebbe essere semplicemente saltare il lavoro se ci sono più di n elementi nella coda. Ciò significherebbe che se si eseguiva rapidamente il backup della coda, si passava rapidamente alla coda e si elaborava solo <n. Se si esegue il backup, la coda di riutilizzo delle celle riceverà un'altra cella e quindi si accoderà di nuovo per caricare l'immagine. Ciò darebbe sempre la priorità al n più recentemente in coda. La cosa di cui non sono sicuro è come il blocco in coda dovrebbe conoscere il numero di elementi in coda. Forse c'è un modo GCD per arrivare a quello? In caso contrario, si potrebbe avere un contatore thread-safe per incrementare / decrementare. Incremento durante l'accodamento, decremento sull'elaborazione. Se lo fai, vorrei aumentare e diminuire come prima riga di codice su entrambi i lati.

Spero che abbia suscitato alcune idee ... potrei giocarci in seguito in codice.


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.


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.)


Le tue proprietà hanno quasi sempre una variabile di supporto. Che cosa

@synthesize searchBar = _searchBar;

si dichiara che la variabile di supporto per la tua barra di ricerca sarà chiamata _searchBar . Questo ti permette di disaccoppiare il nome della proprietà dal nome della tua variabile. Infatti, se non si utilizza @synthesize , non è necessario avere una variabile di backup.

Per quanto riguarda il motivo per cui le persone fanno questo, ognuno ha diverse ragioni. Personalmente, lo faccio

  1. evitare scontri con nomi di variabili e
  2. chiarire quando sto usando una variabile locale e quando sto usando una variabile di istanza.




iphone objective-c ios uitableview grand-central-dispatch