NSProgressIndicator sous El Capitan

Bonjour,


 


Je reviens vers vous après une longue période de codage sans anicroche. Mais j'ai un truc agaçant: un NSProgressIndicator qui a cessé de fonctionner depuis El Capitan.


 


Plus exactement, l'interface n'est plus remise à  jour du tout durant un processus de traitement de fichier: le NSProgressIndicator n'est que la partie visible d'un iceberg qui concerne toute la fenêtre.


 


Le traitement de fichiers s'effectue par lot, dans une boucle. L'interface n'est réactualisée qu'à  la sortie de la boucle, avec les valeurs finales correctes. Un NSLog me montre que tout se déroule bien dans la boucle.


 


J'ai essayé le dessin différé (setNeedsDisplay), immédiat (display), de passer par une animation... rien n'y fait. J'ai adressé un rapport de bug qui s'est révélé un duplicata. Mais peut-être ai-je manqué une marche? il me semble incroyable de devoir écrire une dizaine le lignes de code pour faire avancer un indicateur...


 


D'avance merci si vous avez une façon de contourner le problème en attendant qu'Apple corrige (ou documente) ce nouveau comportement.


Réponses

  • MalaMala Membre, Modérateur

    Tu l'as passé en multi-threadé via la propriété usesThreadedAnimation?


  • berfisberfis Membre

    Non, tout est sur le main thread.


  • CéroceCéroce Membre, Modérateur
    mai 2016 modifié #4
    Rends-tu la main régulièrement pour permettre le rafraà®chissement de l'IHM ? Par rapport à  ce que tu écris, moi, ça m'étonne que ça marchait avant.

    @Mala: je ne trouve pas la doc claire au sujet de .usesThreadedAnimation. Elle parle de l'animation, pas du rafraichissement. De ce que j'en comprends, ça voudrait dire que l'animation de l'état "indéterminé" se poursuivrait, mais pas que changer sa valeur se reflèterait.
    As-tu l'expérience là -dessus ?
  • berfisberfis Membre


    Rends-tu la main régulièrement pour permettre le rafraà®chissement de l'IHM ? Par rapport à  ce que tu écris, moi, ça m'étonne que ça marchait avant.




     


    Il y a une chose qui m'échappe: si le travail effectué sur les fichiers ET la mise à  jour de l'indicateur sont sur le même thread, pourquoi l'interface ne s'actualise-t-elle pas?

  • CéroceCéroce Membre, Modérateur
    mai 2016 modifié #6
    Parce que si tu exécutes un traitement long sur le thread principal, alors tu bloques la runloop de ce thread. Or, tout le dessin se fait sur le thread principal.

    La runloop est une boucle qui:
    1) reçoit les événements
    2) donne la main à  ton code pour y réagir
    3) redessine les vues en appelant leur méthode -drawRect, si l'étape 2 a signalé une demande de rafraichissement avec setNeedsDisplay:

    Si tu lances un traitement long dans 2), alors c'est normal qu'il n'y ait pas de mise à  jour de l'IHM.
    Une solution est de donner la main à  la runloop grâce à  -[NSRunLoop runUntilDate:]. Mais une bien meilleure solution est de faire tes traitements sur des threads secondaires. Ce n'est pas forcément très compliqué, par exemple en utilisant NS(Block)Operation.
  • PyrohPyroh Membre

    C'est quoi exactement ton traitement de fichier ?


  • berfisberfis Membre

    Bonjour et merci à  vous deux pour vos réponses.


    De la réduction d'image jusqu'à  concurrence d'une taille à  ne pas dépasser. C'est plus simple de mettre le code:



    for(NSURL *aFile in files){

    self.indicatorCurrentValue ++;
    [self.indicator display];
    [self setInfoLine:[aFile path]];
    [self.rest setDoubleValue:self.indicator.maxValue-self.indicator.doubleValue];
    [self.rest display];


    NSImage* source = [[NSImage alloc]initByReferencingURL:aFile];
    NSImageRep *rep = [source representations][0];
    NSSize size = NSMakeSize ([rep pixelsWide], [rep pixelsHigh]);
    [source setSize: size];
    float newWidth = source.size.width;
    float newHeight = source.size.height;
    NSData *data;
    do {
    newWidth = newWidth * percentage;
    newHeight = newHeight * percentage;
    NSImage* small = [[NSImage alloc] initWithSize:NSMakeSize(newWidth, newHeight)];
    [small lockFocus];
    [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationMedium];
    [source setSize:NSMakeSize(newWidth, newHeight)];
    [source drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
    [small unlockFocus];
    data = [[NSBitmapImageRep imageRepWithData:[small TIFFRepresentation]] representationUsingType:NSJPEGFileType properties:@{}];
    } while(data.length > (long)UDC_INT(@maxFileSize)*1000);

    [data writeToURL:aFile atomically:NO];
    treatedFiles ++;
    NSLog (@%ld files of %ld, treatedFiles,totalFiles);
    }

    La mise à  jour des indicateurs (progression, nom du fichier en cours de traitement, fichiers restants) s'effectue à  l'intérieur d'une boucle for..in. La valeur percentage est donnée par les préférences, c'est un coefficient de réduction à  appliquer à  chaque itération.


     


    Ce que je ne comprends pas, c'est que le traitement lui-même (la boucle interne do...while) devrait "rendre la main" à  la boucle externe (for...in) et mettre à  jour l'affichage. C'est ce qu'elle faisait sous Yosemite et Mavericks.


  • CéroceCéroce Membre, Modérateur

    C'est ce qu'elle faisait sous Yosemite et Mavericks.

    En fait, il n'y a que cela qui m'étonne.

    Ton code bloque clairement la runloop.
    Je t'ai donné la solution, tu n'as plus qu'à  tester avec [NSRunLoop runUntilDate:], par exemple au bas de ta boucle for...in .
  • MalaMala Membre, Modérateur


    En fait, il n'y a que cela qui m'étonne.


    Ton code bloque clairement la runloop.

    Je t'ai donné la solution, tu n'as plus qu'à  tester avec [NSRunLoop runUntilDate:], par exemple au bas de ta boucle for...in .




    Il force un display. C'est crade mais ça marchait effectivement jusqu'à  présent.


     


     


    Le fait de passer usesThreadedAnimation à  YES doit suffire de mémoire car le changement de valeur fait un setNeedsDisplay de facto. Enfin il me semble. De toute façon, il a une ligne de code à  ajouter pour vérifier donc autant commencer par là .

  • berfisberfis Membre
    mai 2016 modifié #11

    @ Céroce: OK, ça fonctionne effectivement. Mais en admettant que je veuille faire les choses proprement:


    - un fil pour la boucle de traitement ?


    - un fil par traitement ?


    - comment le(s) fil(s) indiquent-ils au fil principal l'avancement de travaux, pour que l'indicateur se mette à  jour ?


     


    @ Mala: oui, c'est crade, mais si je balance des setNeedsDisplay à  la queue-leu-leu, l'indicateur va se mettre à  jour une seule fois, c'est-à -dire quand il sera arrivé à  la fin du traitement, non?


    Bon, 


            [self.indicator setUsesThreadedAnimation:YES];


    semble marcher tout aussi bien. Je continue à  tester.

  • PyrohPyroh Membre

    tu peux utiliser un truc dans le genre: 



    let queue = //tu crée une queue qui permet la concurrence.
    for file in files {
    dispatch_async(queue) {
    self.doSomethingWithThisFile(file)
    dispatch_sync(dispatch_get_main_queue()) {
    // MAJ de l'indicateur
    }
    }
    }

    Je fais ça sans doc donc t'as l'idée en gros mais ça devrait fonctionner.


  • MalaMala Membre, Modérateur


    @ Mala: oui, c'est crade, mais si je balance des setNeedsDisplay à  la queue-leu-leu, l'indicateur va se mettre à  jour une seule fois, c'est-à -dire quand il sera arrivé à  la fin du traitement, non?




    Normal. Céroce a expliquer le pourquoi.


     


    Et en passant la barre de progression en mutli-threadé, tu n'as ainsi pas à  te soucier du refresh. C'est historiquement fait pour ça.

  • CéroceCéroce Membre, Modérateur

    - un fil pour la boucle de traitement ?

    - un fil par traitement ?

    - comment le(s) fil(s) indiquent-ils au fil principal l'avancement de travaux, pour que l'indicateur se mette à  jour ?



    ça dépend de comment tu souhaites informer de l'avancement. Disons que tu veux que la barre progresse à  chaque fichier traité, voici comment je m'y prendrais, je crois:

    - créer une NSOperationQueue

    - créer une NSBlockOperation par fichier à  traiter.

    - fixer leur propriété .completionBlock. C'est ce qui permet de savoir que le traitement d'un fichier est terminé

    - quand le completionBlock est appelé, changer la valeur du progress indicator. Attention, c'est à  faire sur le thread principal. Il faut aussi regarder le nombre d'opérations restantes dans la queue. Si c'est zéro, alors c'est terminé.


    La propriété NSOperationQueue.maxConcurrentOperationCount peut rester à  sa valeur par défaut. Dans ce cas, c'est OS X qui détermine le nombre de threads à  créer. Les opérations de traitement des fichiers vont donc s'exécuter en parallèle, et ça ira plus vite qu'avant. Si tu mets la propriété à  1, alors tu as une queue série, avec un seul thread secondaire.


    Je vois un problème plus que potentiel dans ton code:



    [small lockFocus];
    [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationMedium];
    [source setSize:NSMakeSize(newWidth, newHeight)];
    [source drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
    [small unlockFocus];

    Les opérations s'exécutant en parallèle, ça va mettre un gros souk dans les lockFocus et unlockFocus. Tu n'auras aucune idée de si currentContext correspond bien à  l'image traitée.

    Personnellement, je règlerais ça en utilisant un CGBitmapContext. Ainsi, il n'y a pas d'histoire de "contexte courant", puisqu'on passe le contexte en paramètre à  chaque fois, et on évite tout problème.


    (En passant: quand on dit qu'il ne faut pas utiliser de singleton, comme ici [NSGraphicsContext currentContext], il y a une bonne raison. En fait, plusieurs).


     


    Pyroh proposait de passer par GCD plutôt que par NSOperation/Queue, c'est une option à  considérer, pouvant rendre le code plus simple.

     


  • Mon Dieu, je finis avec des trucs comme ça:



    __weak __typeof__(self) weakSelf = self;
    dispatch_group_async(_operationsGroup, _operationsQueue, ^
    {
    __typeof__(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doSomethingElse];
    } );

    Mais au moins je n'ai plus ce fichu ballon de plage...


    Merci pour ces conseils, j'ai dû pas mal ramer dans la doc, sur SO et autres...


  • Joanna CarterJoanna Carter Membre, Modérateur
    Comme attendu ;)
Connectez-vous ou Inscrivez-vous pour répondre.