Performances UICollectionView et GCD

Hello,


 


Je me heurte à  un problème de performances avec l'utilisation des UICollectionView. ( C'est un calendrier)


 


Voci un bout de mon code :



- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
   
    SGCalendarViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:[SGCalendarViewCell defaultIdentifier] forIndexPath:indexPath];
   
[cell setSelected:(indexPath == self.selectedDateIndexPath)];
    
    dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(myQueue, ^{
        NSDate *date = [self dateForIndexPath:indexPath];
        dispatch_async(dispatch_get_main_queue(), ^{
            
           });
    });

 return cell;
}

Le problème est dans la ligne : 



NSDate *date = [self dateForIndexPath:indexPath];

Si je commente cette ligne, l'interface ne se freeze pas mais si je la laisse l'application n'est pas utilisable.


 


Je ne comprends pas parce que je crée une queue concurrente pour dispatcher les blocks qui prennent du temps et ça pour ne pas bloquer le thread principal.


 


J'ai l'impression que le system utilise le thread principal même si je crée une queue concurrente.


 


Bon je suis perdu :)


 


Merci pour votre aide.  


 


 


 


Réponses

  • AliGatorAliGator Membre, Modérateur
    Le code s'exécute bien dans un thread secondaire (s'il s'exécutait dans le thread principal, dans ce cas tu aurais ton interface qui "freeze". Ce qui se passe c'est que ton code s'exécute bien dans un thread secondaire, mais ta cellule étant très probablement recyclée entre temps, du coup ce que tu fais de la valeur récupérée n'a plus de sens, car ce n'est plus pour la même date, la cellule ayant été recyclée entre temps

    En effet, ce n'est pas très logique de dispatcher cette ligne dans un thread secondaire alors que tu retournes la cell tout de suite : dans ce cas la CollectionView va afficher la cell sans bloquer c'est vrai, et le calcul de la date pour l'indexPath donné, lui, va se faire en background... mais vu le mécanisme de recyclage des cellules, que vas-tu faire une fois que la NSDate aura été récupérée ? Si tu fais un truc comme "cell.dateLabel.text = [self qqchAvecTaDate]" forcément ça va être inutilisable, car au moment où ce code va s'exécuter, ta cellule aura eu largement le temps de se recycler et ne représentera alors plus la même date.

    C'est le même problème que quand on veux afficher une image téléchargée depuis le net dans une UITableViewCell ou une UICollectionViewCell, le temps de télécharger l'image (de manière asynchrone pour ne pas bloquer le thread principal), la cellule a eu le temps de se recycler, et si une fois que tu as téléchargé l'image tu te contentes de l'affecter à  une UIImageView de ta même cell, ça n'aura plus de sens car cette cell ne représentera plus le même objet de ton modèle entre temps, ayant été recyclée.



    De deux choses l'une :

    1) Soit ton calcul d'une date pour un NSIndexPath donné est vraiment long, et la longueur de ce calcul est justifiée et ne peut pas être optimisée. Dans ce cas, il faut probablement pré-calculer cette valeur à  l'avance, mettre en place des mécanismes de cache pour mémoriser la date et éviter de la recalculer à  chaque fois si ce calcul est si consommateur, et si tu veux conserver un tel mécanisme d'asynchronisme, vérifier une fois que tu as la valeur que la cellule n'a pas été recyclée entre temps.

    2) Soit ton calcule d'une date pour un NSIndexPath donné peut être grandement optimisé et n'est pas si consommateur de ressources une fois que tu le fais correctement et pas n'importe comment, et dans ce cas tu pourrais appeler ta méthode "dateForIndexPath:" directement sur le thread principal de façon synchrone, si ce calcul est suffisamment rapide. Même si ça n'empêche pas de mettre en place un mini-cache (façon NSCache) au fur et à  mesure pour accélérer encore les choses.

    A mon avis, tu es dans le second cas. D'autant que si tu utilises un NSDateFormatter, comme c'est une classe qui prend du temps à  allouer, si tu alloues un NSDateFormatter à  chaque fois que tu appelles ta méthode (plutôt que d'utiliser toujours le même), c'est sûr et certain que cette méthode va prendre du temps et plomber tes performances. Si tu nous montres le code de ta méthode dateForIndexPath: je suis sûr qu'il y a des soucis dedans et un moyen de l'optimiser grandement son code de sorte qu'elle s'exécute rapidement et qu'il n'y ait plus besoin de l'exécuter sur un thread secondaire, ce qui simplifierait les choses et éviterait de mettre en place toute la mécanique de prise en compte du recyclage des cellules quand on fait des appels asynchrones dans cellForRowAtIndexPath " ce qui de toute façon vu le mécanisme de recyclage est souvent une mauvaise idée si on ne met pas les précautions nécessaires.
  • Oui je pense aussi que je suis dans le deuxième cas et que mon code n'est pas optimisé. J'ai rajouté le système de queue juste pour tester et après êtres désespéré d'optimiser le code.


     


    Voici le code de la méthode :



    - (NSDate *)dateForIndexPath:(NSIndexPath *)indexPath {
        NSDate *date = [self.startDate dateByAddingMonths:indexPath.section];
        NSDateComponents *components = [date components];
        components.day = indexPath.item + 1;
        date = [NSDate dateFromComponents:components];
        
        NSInteger offset = [self offsetForSection:indexPath.section];
        if (offset) {
            date = [date dateByAddingDays:-offset];
        }
        return date;
    }

    Catégorie sur NSDate :



    - (NSDate *)dateByAddingMonths:(NSInteger)months {
        NSDateComponents *components = [NSDateComponents new];
        components.month = months;
        
        NSCalendar *calendar = FixedCalendar();
        return [calendar dateByAddingComponents:components toDate:self options:0];
    }


    NSCalendar *FixedCalendar () {
        NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
        return calendar;
    }


    + (NSDate *)dateFromComponents:(NSDateComponents *)components {
        return SGCalendarDateFromComponents(components);
    }


    NSDate * SGCalendarDateFromComponents(NSDateComponents *components) {
        NSCalendar *calendar = FixedCalendar();
        return [calendar dateFromComponents:components];
    }


    - (NSInteger)offsetForSection:(NSInteger)section {
        NSDate *firstDayOfMonth = [self dateForFirstDayOfSection:section];
        return [firstDayOfMonth weekday] - 2;
    }


    - (NSDate *)dateForFirstDayOfSection:(NSInteger)section {
        return [[self.startDate firstDayOfMonth] dateByAddingMonths:section];
    }


    - (NSDate *)firstDayOfMonth {
        NSDateComponents *components = SGCalendarDateComponentsFromDate(self);
        [components setDay:1];
        return SGCalendarDateFromComponents(components);
    }

    Voila le code de la méthode qui freeze l'application.


    Je vois une optimisation sur le calendrier que je crée a chaque appel de FixedCalendar à  part ça j'arrive pas à  optimiser plus.


     


    voyez vous d'autres conneries :) ?  Merci

  • AliGatorAliGator Membre, Modérateur
    La première chose à  faire est de lancer un Time Profiler sur ton code avec Instruments (Command-I, ou commande du menu Product) pour voir ce qui prend le + de temps et donc savoir quelle sont vraiment les lignes ou méthodes à  optimiser et sur lesquelles il faut passer du temps.
  • samirsamir Membre
    décembre 2014 modifié #5

    80 % des performances étaient le fait que je crée une instance de NSCalendar à  chaque appel de cellForRow.... je me suis pas rendu compte parce que je croyais que j'utilisais le currentCalendar de NSCalendar... donc je me retrouve avec des milliers d'instances :).


     


    Il reste encore des performances à  améliorées si vous voyez quelques choses ou bien carrément une autre logique ou manière de faire. Sinon je vais revoir tout ça à  tete reposée. 


     


    Et c'est pas évident le développement B)  . 


     


    Merci AliGator.


  • AliGatorAliGator Membre, Modérateur
    Si je comprend bien, le but de ta méthode dateForIndexPath est :
    - de partir d'une startDate
    - d'y ajouter comme nombre de mois indexPath.section
    - d'y ajouter comme nombre de jours indexPath.item + offset
    - où "offset" est l'index du jour de la semaine où tombe le 1er du mois... -2

    1) D'une part, c'est très dommage de ne pas arrêter de basculer de NSDate à  NSDateComponents dans tous les sens, au lieu de conserver des NSDateComponents tout du long et de ne reconvertir en NSDate qu'au dernier moment
    2) D'autre part, l'offset pour un mois donné pourrait même être mis en cache pour ne pas la recalculer pour chaque case de ton calendrier

    Déjà  pour le 1, moi je ferais :
    - (NSDate *)dateForIndexPath:(NSIndexPath *)indexPath {
    NSDateComponents *components = [self.startDate components]; // (1)
    components.month += indexPath.section;
    components.day += indexPath.item + [self firstDayOffsetForComponents:components];
    return [self.calendar dateFromComponents:components];
    }

    - (NSInteger)firstDayOffsetForComponents:(NSDateComponents*)components
    {
    // On prend la date en question, mais on remet le jour à  1 pour se recaler sur le premier du mois
    NSDateComponents* comps = [components copy]; // pour éviter de modifier l'original
    comps.day = 1;
    return [[self.calendar dateFromComponents:comps] weekday] - 2;
    }
    Où "calendar" est une "@property(strong) NSCalendar* calendar" que tu initialises au début pour qu'elle contienne le NSCalendar grégorien.

    (1) Et encore, à  se demander si tu ne devrais pas stocker directement les NSDateComponents de ta date de début plutôt que sa forme sous NSDate, pour pas avoir à  la convertir à  chaque fois ?

    Et encore comme j'ai dit pour la méthode qui calcule l'offset pour chaque mois, ça serait vraiment + efficace de calculer ces offsets une fois pour toutes en avance de phase plutôt que de les recalculer pour chaque NSIndexPath " alors que pour chaque cellule, ça dépend que de la section, donc si tu as 31 cellules dans une section, tu vas faire le calcul 31 fois pour un mois donné qui va à  chaque fois donner le même résultat, c'est un peu dommage... Soit un NSCache (dont la clé serait le mois + année par exemple, et la valeur l'offset pour ce mois), soit un simple NSArray dans lequel tu stockes les offsets précalculés pour chaque indexPath.section.


    Bref, il y a vraiment moyen d'optimiser, la première des choses à  faire étant d'éviter de convertir des NSDate en NSDateComponents en NSDate en NSDateComponents, etc... mais de garder des NSDateComponents tout le long le temps de faire tes calculs, et ne reconvertir en NSDate qu'à  la fin. Ca évite de faire des conversions dans tous les sens pour pas grand chose, et ça simplifie aussi la lecture et les calculs. Regarde par exemple comment j'ai juste ajouté à  la fois les mois et les jours en 2 lignes au NSDateComponent initial, pas besoin de faire des aller-retours vers NSDate pour ça.
  • AliGatorAliGator Membre, Modérateur

    Et c'est pas évident le développement B)  .

    Personne n'a dit que c'était évident, c'est pour ça que c'est un métier à  part entière.
Connectez-vous ou Inscrivez-vous pour répondre.