Optimisation d'une boucle comprenant des traitements asynchrones

Alf1996Alf1996 Membre
juin 2014 modifié dans Objective-C, Swift, C, C++ #1

Bonjour à  tous,


Avant de me mettre à  Swift, je suis en train de finaliser une mise à  jour de mon application et j'ai rencontré un petit problème. J'ai fini par résoudre mon problème mais comme je trouve ma solution un peu bourrin, je viens chercher votre avis pour savoir si vous auriez une autre solution.


 


Initialement, mon application couvrait seulement l'Europe, et je viens d'ajouter la possibilité d'accéder au reste du monde, en ajoutant des "in app purchase". La conséquence est que je dois maintenant importer mes données sous la forme d'une boucle sur le nombre de zones acquises par l'utilisateur. A l'intérieur de la boucle, des appels à  la méthode d'importation et de traitement des données sont fait en asynchrone (requêtes URL, puis traitement). Mon problème est que chaque élément de boucle n'est pas forcément traité à  la même vitesse, et j'ai donc un problème pour savoir quand faire disparaitre ma fenêtre "activité en cours".


J'ai résolu çà  de la façon suivante :



MyActivityView *myActivityView=[[MyActivityView alloc] init];
myActivityView.labelTextString=NSLocalizedString(@Chargement des informations en cours...,@Veuillez patienter - Chargement des informations en cours...);
// Ajout en temps que subview
[self.view addSubview:myActivityView.view];

for (NSString *region in self.listOfSelectedRegions) {
// Méthode d'importation de données comportant des requêtes asynchrones, avec execution d'un " block completion "
[self importDataForRegion:region withCompletion:^(NSArray* data,NSError* errorData) {
if (data!=nil && [data count]!=0 && errorData==nil) {
// Bla bla bla... Traitement des données
nbRegionsFinished++;
// Si on est sur la dernière région... suppression de la fenêtre d'activité
if (nbRegionsFinished==[self.listOfSelectedRegions count]) {
// Sauvegarde des données importées, puis...

// Importation terminée - suppression de la subview "Activité en cours"
[myActivityView.view removeFromSuperview];
}
} else {
// Bla bla bla... Traitement des erreurs...
// Interruption du traitement et suppression de la subview "Activité en cours"
[myActivityView.view removeFromSuperview];
}
}];
}


Avez vous une solution plus élégante ?


Merci d'avance

Réponses

  • AliGatorAliGator Membre, Modérateur
    Comme solution alternative y'a bien :
    - soit les dispatch_group
    - soit NSOperationQueue avec une NSOperation finale qui a une dépendance vers toutes les autres et ne s'exécuterait alors que quand toutes les autres sont terminées
  • OK merci


    Sinon, ma solution, elle t'a pas trop fait bondir ? 


     


    Je vais regarder les dispatch_group dans un premier temps. Merci


  • AliGatorAliGator Membre, Modérateur
    juin 2014 modifié #4
    Honnêtement non ça va. Je comprend ta volonté de faire plus propre, je chercherais aussi comme toi un truc plus joli si j'avais à  faire ça, mais en attendant ta solution n'est pas non plus affreuse.


    Je ferais juste une capture dans une variable locale de self.listOfSelectedRegions (éventuellement même en copy) ou au moins une capture de son count, histoire d'être sur de pas avoir des comportements bizarres si jamais cette liste venait à  changer pendant que tu aurais encore des tâches en cours et donc que le count aurait varié entre le début de ta boucle et l'exécution des completionBlocks.


  • Je ferais juste une capture dans une variable locale de self.listOfSelectedRegions (éventuellement même en copy) ou au moins une capture de son count, histoire d'être sur de pas avoir des comportements bizarres si jamais cette liste venait à  changer pendant que tu aurais encore des tâches en cours et donc que le count aurait varié entre le début de ta boucle et l'exécution des completionBlocks.




     


    Oui, en réalité, c'est ce que j'ai fait (capture du count), mais pour la lisibilité de l'exemple de code que j'ai fourni, j'ai retiré cette variable. Quoi qu'il en soit, la dite variable de doit pas changer à  ce stade là , car elle n'évolue que très rarement (au moment de l'achat intégré, et éventuellement par l'accès aux options, qui est inopérant à  ce moment de l'exécution).


    Je vais probablement soumettre comme çà , et à  côté essayer de faire un peu plus propre.


    Merci en tout cas pour ton aide.

  • FKDEVFKDEV Membre
    juin 2014 modifié #6

    A ta place, pour un projet perso, je m'arrêterais là .


     


    Pour un projet pro, si j'ai du temps et des perspectives d'évolutions ou des besoins de tests, j'essaierai de séparer le traitement d'affichage et le traitement de download.


     


    C'est toujours un bon point de départ quand tu sens qu'il y a un truc qui ne vas pas de séparer les traitements GUI du reste et de donner des responsabilités à  des objets en se posant la question : et si je voulais lancer ce traitement en batch ou en ligne de commande ? (ou tout simplement en background fetching)


     


    Ce que je ferais :


    "NSString* region" deviendrait un objet d'une classe à  part entière (ALFFetchRegion).


    importDataForRegion serait donc une méthode de cette classe.


    Dans cette classe tu pourrais aussi mettre des données d'état (en cours de téléchargement, fini, etc).


    Tu pourrais aussi y des fonctions utilitaires comme calculer l'url de la region, etc. Si tu as un objet modèle "région", cette classe aurait un lien vers l'objet modèle pour pouvoir le completer à  la fin du téléchargement.


    Ensuite, tu pourrais prévenir l'UI via un protocole/delegate (didStartDownload, didFinishDownload, etc).


    Ou bien l'UI pourrait observer le status de cet objet via les KVO.


     


    Ton compteur de tâche en cours serait dans ton ViewController et il serait décrementé à  chaque fois qu'un objet ALFFetchRegion termine son traitement.


    Assez vite, tu risques d'avoir besoin d'un objet manager à  part entière pour gérer cette liste de téléchargement mais tu peux t'en passer si l'utilisateur est bloqué sur un ViewController quand le téléchargement a lieu.


  • Alf t'as pas du boulot à  faire, plutôt que d'optimiser une boucle ? Mais où est donc passé le MH370 ?
  • samirsamir Membre
    juin 2014 modifié #8

    Hello,


     


    ( Quelques remarques en plus, peut être qui t'intéresse le moins :))


     


    Moi je commencerai par supprimer tous les commentaires que tu as mis.


     


    Exemple:


    1.



    // Ajout en temps que subview
    [self.view addSubview:myActivityView.view];

    Je trouve que le commentaire n'a aucune valeur ajouté à  la ligne du code.


     


    2.



    NSLocalizedString(@Chargement des informations en cours...,@Veuillez patienter - Chargement des informations en cours...);

    Personnellement j'utilise des constantes clés avec des préfixes.



    NSLocalizedString(@DOWNLOAD_PROGRESS_.....,@description........);

    3.



     if (nbRegionsFinished==[self.listOfSelectedRegions count])

    Je pense que tu peux éviter la variable nbRegionsFinished en testant directement



    if ([region isEqual...[self.listOfSelectedRegions lastObject]]

    4.


    Je fais souvent une capture du self avant de l'utiliser dans un block histoire de ne pas créer des retain cycle 



    __weak MySelfType *blocksafeSelf = self;

  • @FKDEV Merci pour ton intervention. Très intéressant pour moi de lire une idée d'architecture sur un cas concret.




  •  


    Hello,


     


    ( Quelques remarques en plus, peut être qui t'intéresse le moins :))


     


    Moi je commencerai par supprimer tous les commentaires que tu as mis.


     


    Exemple:


    1.



    // Ajout en temps que subview
    [self.view addSubview:myActivityView.view];

    Je trouve que le commentaire n'a aucune valeur ajouté à  la ligne du code.


     


    2.



    NSLocalizedString(@Chargement des informations en cours...,@Veuillez patienter - Chargement des informations en cours...);

    Personnellement j'utilise des constantes clés avec des préfixes.



    NSLocalizedString(@DOWNLOAD_PROGRESS_.....,@description........);

    3.



     if (nbRegionsFinished==[self.listOfSelectedRegions count])

    Je pense que tu peux éviter la variable nbRegionsFinished en testant directement



    if ([region isEqual...[self.listOfSelectedRegions lastObject]]

    4.


    Je fais souvent une capture du self avant de l'utiliser dans un block histoire de ne pas créer des retain cycle 



    __weak MySelfType *blocksafeSelf = self;



     


    1. Les commentaires ont uniquement été ajoutés pour le code "exemple". Je te l'accorde, celui-là  ne servait pas à  grand chose !


     


    2. et 4. Merci pour l'info


     


    3. Non, çà  ne fonctionne pas. Comme je l'expliquai dans mon premier post, les différents éléments de la boucle ne finissent pas forcément dans l'ordre du lancement, c'est bien ce qui m'a posé souci. En faisant ainsi, ma fenêtre d'activité pouvait s'arrêter sur le dernier élément lancé, mais qui étant plus rapide que les autres, avait terminé en premier. D'où la nécessité de compter les éléments au fur et à  mesure qu'il était terminés (entrée dans le completion block)


     


    @ FKDEV : merci pour tous ces conseils. Effectivement, ce serait une bonne solution. Je me rend compte à  chaque mise à  jour que je découvrais objective-C lorsque j'ai commencé cette application. J'améliore les choses au fur et à  mesure, et je dois avouer que parfois, les modifications requises sont lourdes.


    D'où l'intérêt de bien penser le modèle avant de se lancer dans l'aventure !!!

  • AliGatorAliGator Membre, Modérateur
    juin 2014 modifié #11
    @samir

    3. Non, au contraire, puisque les tâches ne sont pas séquentielles mais parallèles et donc ce n'est pas forcément la dernière démarrée qui se finira en dernier, c'est tout le problème de départ.

    4. Ce n'est pas toujours le truc à  faire. Et ta solution est beaucoup trop simpliste.

    A/ Il ne faut le faire que quand c'est justifié, c'est à  dire quand le block retient self d'un côté mais que self retient aussi le block. Par exemple si ton objet (self) a une "@property(strong) void(^)(void) completionBlock", et que tu écris "self.completionBlock = ^{ ... du code qui utilise self ... }" alors self est capturé par le block, et le block est retenu par self de par la @property, d'où retain cycle. Et dans ce cas oui il faut utiliser un weakSelf

    B/ Par contre si ce n'est pas le cas, par exemple si jamais le block ne retient pas self et self ne retient pas le block non plus, plus personne ne se retient et au contraire tu risques d'avoir des objets ou des blocks qui, n'étant plus retenus par personne, vont être désalloués. Si dans mon même exemple ci-dessus ta @property est weak et pas strong, et que tu utilises ton "__weak ... blocksafeSelf", alors tu auras des problèmes...

    C/ S'il est justifié de créer un weakSelf (pour casser un retainCycle qu'il y aurait sinon parce que le block est déjà  retenu par self donc il faut que le block utilise un weakSelf plutôt que self pour pas avoir un cycle), alors il ne faut pas oublier de recréer un strongSelf dès l'entrée dans le block, pour en refaire une référence forte, et ne pas risquer que ce weakSelf disparaisse et soit remis à  nil... pile (pas de bol) en plein milieu de l'exécution de ton block (c'est un risque avec le multithreading), ce qui ferait alors que pendant le début de ton block weakSelf (blocksafeSelf) contiendrait bien ton objet, mais pendant l'autre moitié blocksafeSelf serait passé à  nil entre temps...


    Donc si l'utilisation d'un weakSelf est justifiée, alors le pattern à  utiliser en général ressemble plutôt à  ceci :
    __weak typeof(self) weakSelf = self; // pour ne pas risquer de capturer self
    // Imaginons que completionBlock est une @property(strong) de self
    self.completionBlock = ^{
    // Du coup ne surtout pas utiliser self nulle part dans ce block pour ne pas que self soit capturé et crée un retainCycle
    // Mais si on utilise weakSelf à  plusieurs endroits dans le code, on risque aussi qu'il soit non-nil au début mais nil à  la fin
    // Donc recréons une référence forte qui va à  nouveau s'assurer qu'on garde en vie notre objet au moins pendant la durée d'exécution du block
    __strong typeof(self) strongSelf = weakSelf; // On reconverti weakSelf en une variable strong pour être sûr que l'objet self soit retenu localement le temps du block

    ... du code qui va utiliser strongSelf (mais pas weakSelf et surtout encore moins self) ...
    };
    Donc en résumé, déjà  ne pas utiliser forcément "__weak self" si ce n'est pas justifié (car ça pourrait avoir l'effet inverse), mais de plus si on l'utilise penser à  le "re-strong-ifier" dès l'entrée dans la boucle.
  • Merci beaucoup pour ces précisions.


     


    C'est vrai que j'utilise ce faux pattern souvent, mais les risques de bugs sont minimes parce que  généralement (souvent ?) si on déclare un block comme property c'est forcement une référence forte sinon ça n'a pas de sens à  mon avis.


     


    Merci encore :)


  • AliGatorAliGator Membre, Modérateur
    juin 2014 modifié #13
    Oui sauf qu'il y a des fois on ne déclare pas du tout ce block en property (mais en variable locale, par exemple...)
Connectez-vous ou Inscrivez-vous pour répondre.