27 votes

Pile "Last In-First Out" avec GCD ?

J'ai une UITableView qui affiche les images associées aux contacts dans chaque ligne. Dans certains cas, ces images sont lues au premier affichage à partir de l'image du contact du carnet d'adresses, et lorsqu'il n'y en a pas, il s'agit d'un avatar rendu sur la base de données stockées. Actuellement, ces images sont mises à jour sur un fil d'arrière-plan en utilisant GCD. Cependant, cela charge les images dans l'ordre où elles ont été demandées, ce qui signifie que lors d'un défilement rapide, la file d'attente devient longue et que lorsque l'utilisateur arrête le défilement, les cellules actuelles sont les suivantes dernier pour être mis à jour. Sur l'iPhone 4, le problème n'est pas vraiment perceptible, mais je tiens à soutenir le matériel plus ancien et je fais des tests sur un iPhone 3G. Le retard est tolérable mais tout à fait perceptible.

Il me semble qu'une pile "Last In-First Out" serait susceptible de résoudre en grande partie ce problème, car chaque fois que l'utilisateur arrête de défiler, ces cellules seraient les prochaines à être mises à jour, puis les autres qui sont actuellement hors écran seraient mises à jour. Une telle chose est-elle possible avec Grand Central Dispatch ? Ou n'est-il pas trop onéreux de l'implémenter d'une autre manière ?

Notez, en passant, que j'utilise Core Data avec un magasin SQLite et que je n'utilise pas de NSFetchedResultsController. en raison d'une relation many-to-many qui doit être traversée afin de charger les données de cette vue. (Pour autant que je sache, cela exclut l'utilisation d'un NSFetchedResultsController). [J'ai découvert qu'un NSFetchedResultsController peut être utilisé avec des relations many-to-many, malgré ce que semble dire la documentation officielle. Mais je n'en utilise pas encore un dans ce contexte].

Ajout : Je tiens à préciser que si le sujet est "Comment créer une pile Last In-First Out avec GCD", en réalité, je veux juste résoudre le problème décrit ci-dessus et il y a peut-être une meilleure façon de le faire. Je suis plus qu'ouvert aux suggestions comme celle de timthetoolman qui résout le problème décrit d'une autre manière ; si une telle suggestion est finalement ce que j'utilise, je reconnaîtrai à la fois la meilleure réponse à la question originale et la meilleure solution que j'ai fini par mettre en oeuvre... :)

16voto

timthetoolman Points 3753

En raison des contraintes de mémoire du dispositif, vous devez charger les images à la demande et sur une file d'attente GCD en arrière-plan. Dans la méthode cellForRowAtIndexPath :, vérifiez si l'image de votre contact est nulle ou a été mise en cache. Si l'image est nulle ou n'est pas dans le cache, utilisez un dispatch_async imbriqué pour charger l'image depuis la base de données et mettre à jour la cellule du 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;
}

La vidéo de la conférence WWDC2010 "Introducing Blocks and Grand Central Dispatch" montre un exemple utilisant également le dispatch_async imbriqué.

Une autre optimisation potentielle pourrait être de commencer à télécharger les images dans une file d'attente en arrière-plan de faible priorité au lancement de l'application.

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

        });
 });

Avec cette répartition imbriquée, nous rendons les images sur une file d'attente à priorité extrêmement basse. Le fait de placer le rendu de l'image dans la file d'attente prioritaire de l'arrière-plan permettra aux images rendues par la méthode cellForRowAtIndexPath ci-dessus d'être rendues à une priorité plus élevée. Ainsi, en raison de la différence de priorité des files d'attente, vous aurez un LIFO "du pauvre".

Bonne chance.

11voto

Duncan Babbage Points 3732

Le code ci-dessous crée une pile flexible de type "last in-first out" qui est traitée en arrière-plan à l'aide de Grand Central Dispatch. La classe SYNStackController est générique et réutilisable, mais cet exemple fournit également le code pour le cas d'utilisation identifié dans la question : rendre les images des cellules du tableau de manière asynchrone, et s'assurer que lorsque le défilement rapide s'arrête, les cellules actuellement affichées sont les prochaines à être mises à jour.

Félicitations à Ben M. dont la réponse à cette question a fourni le code initial sur lequel il s'est basé. (Sa réponse fournit également du code que vous pouvez utiliser pour tester la pile.) L'implémentation fournie ici ne nécessite pas d'ARC, et utilise uniquement Grand Central Dispatch plutôt que performSelectorInBackground. Le code ci-dessous stocke également une référence à la cellule actuelle en utilisant objc_setAssociatedObject qui permettra à l'image rendue d'être associée à la cellule correcte, lorsque l'image sera chargée ultérieurement de manière asynchrone. Sans ce code, les images rendues pour les contacts précédents seront incorrectement insérées dans les cellules réutilisées, même si elles affichent maintenant un contact différent.

J'ai attribué la prime à Ben M. mais je marque cette réponse comme étant la réponse acceptée puisque ce code est plus complètement travaillé.

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

Dans le view.h, avant @interface :

@class SYNStackController;

Dans la section view.h @interface :

SYNStackController *stackController;

Dans le fichier view.h, après la section @interface :

@property (nonatomic, retain) SYNStackController *stackController;

Dans le view.m, avant @implementation :

#import "SYNStackController.h"

Dans la vue.m viewDidLoad :

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

Dans la vue.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;
}

6voto

Ben M. Points 300

Ok, j'ai testé et ça marche. L'objet tire simplement le bloc suivant de la pile et l'exécute de manière asynchrone. Actuellement, cela ne fonctionne qu'avec les blocs de retour de type void, mais vous pourriez faire quelque chose de fantaisiste comme ajouter un objet qui aura un bloc et un délégué pour lui passer le type de retour du bloc.

NOTE : J'ai utilisé ARC dans ce cas, donc vous aurez besoin de XCode 4.2 ou plus, pour ceux d'entre vous qui utilisent des versions plus récentes, il suffit de changer le strong en retain et tout devrait bien se passer, mais il y aura une fuite de mémoire si vous n'ajoutez pas les releases.

EDIT : Pour être plus spécifique à votre cas d'utilisation, si votre TableViewCell a une image, j'utiliserais ma classe stack de la façon suivante pour obtenir les performances que vous souhaitez, s'il vous plaît laissez-moi savoir si cela fonctionne bien pour vous.

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

Voici le code de test qui montre que cela fonctionne comme une pile :

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

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

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

        sleep(1);

    }];
}

Voici le fichier .h :

#import <Foundation/Foundation.h>

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

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

@end

Et le .m :

#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

3voto

bryanmac Points 22834

Je n'ai pas encore essayé - je ne fais que lancer des idées.

Vous pourriez maintenir votre propre pile. Ajoutez à la pile et mettez en file d'attente pour GCD sur le thread de premier plan. Le bloc de code que vous mettez en file d'attente pour GCD extrait simplement le bloc suivant de votre pile (la pile elle-même aurait besoin d'une synchronisation interne pour le push & pop) et l'exécute.

Une autre option peut être de simplement sauter le travail s'il y a plus de n éléments dans la file d'attente. Cela signifierait que si vous faites rapidement reculer la file d'attente, elle passerait rapidement à travers la file d'attente et ne traiterait que < n. Si vous faites reculer, la file d'attente de réutilisation des cellules, obtiendrait une autre cellule et ensuite vous la remettriez en file d'attente pour charger l'image. Cela donnerait toujours la priorité aux n plus récentes files d'attente. Ce dont je ne suis pas sûr, c'est de la manière dont le bloc mis en file d'attente connaîtrait le nombre d'éléments dans la file. Peut-être existe-t-il une méthode GCD pour y parvenir ? Sinon, vous pourriez avoir un compteur threadsafe à incrémenter/décrémenter. Incrémenter lors de la mise en file d'attente, décrémenter lors du traitement. Si vous faites cela, l'incrémentation et la décrémentation seraient la première ligne de code des deux côtés.

J'espère que cela a suscité quelques idées ... Je pourrais y jouer plus tard dans le code.

3voto

rgeorge Points 4568

Une méthode simple qui peut s'avérer suffisante pour votre tâche : utiliser NSOperation La fonction "Dépendances".

Lorsque vous devez soumettre une opération, récupérez les opérations de la file d'attente et recherchez l'opération la plus récemment soumise (c'est-à-dire en remontant depuis la fin du tableau) qui n'a pas encore été lancée. Si une telle opération existe, paramétrez-la pour qu'elle dépende de votre nouvelle opération avec la commande addDependency: . Puis ajoutez votre nouvelle opération.

Cela construit une chaîne de dépendance inverse à travers les opérations non lancées qui les obligera à s'exécuter en série, selon le principe du dernier entré, premier sorti, selon la disponibilité. Si vous voulez permettre n (> 1) à exécuter simultanément : trouvez le n l'opération non démarrée la plus récemment ajoutée et ajoute la dépendance à celle-ci. (et bien sûr, définissez le paramètre maxConcurrentOperationCount a n .) Il y a des cas limites où cela ne sera pas 100% LIFO mais cela devrait suffire pour le jazz.

(Cela ne couvre pas les opérations de re-priorisation si (par exemple) un utilisateur fait défiler la liste vers le bas, puis remonte un peu, le tout plus vite que la file d'attente ne peut remplir les images. Si vous voulez vous attaquer à ce cas, et si vous vous êtes donné un moyen de localiser l'opération correspondante déjà mise en file d'attente mais non démarrée, vous pouvez effacer les dépendances de cette opération. Cela la ramène effectivement en "tête de ligne". Mais puisque le pur "premier entré, premier sorti" est presque déjà assez bien, vous n'aurez peut-être pas besoin de faire cette fantaisie).

[modifié pour ajouter :]

J'ai mis en œuvre quelque chose de très similaire - un tableau des utilisateurs, leurs avatars récupérés paresseusement à partir de gravatar.com en arrière-plan - et cette astuce a très bien fonctionné. L'ancien code était :

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

qui est devenu :

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

Dans le premier cas, les icônes s'affichent visiblement de haut en bas. Dans le second, l'icône du haut se charge, puis les autres se chargent de bas en haut ; et le défilement rapide vers le bas provoque un chargement occasionnel, puis un chargement immédiat (de bas en haut) des icônes de l'écran où vous vous arrêtez. C'est très élégant, l'application est beaucoup plus rapide.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X