Recyclage des UITableViewCell c'est bien beau mais comment font les pro ?

Alors voilà  il y'a delà  une semaine j'ai compris grâce à  qqun qui m'a obligé à  lire la doc dont je ne citerai le nom (c'est un homme-crocodile : indice) le concept de recyclage en lisant la doc de 98 pages.


 


Bon bah j'ai compris comment marché la queue de recyclage, quand une cellule est rappelé à  l'ordre et quand une est mise dans la queue bref j'ai compris le concept générale.


 


Maintenant je me demande comment se débrouille (attention je ne fait pas de pubs) les applications tels que Twitter/Tweetbot/Facebook/Instagram et bien d'autres pour se trimbaler avec des milliers de UITableViewCell sans lag, sans problème de recyclage et bien d'autres.


 


Je parles des grosse entreprise mais je pourrait prendre l'exemple des application pour suivre l'actualité iPhone (MacGeneration, iphoneAddict, appsystem .. encore une fois je ne fait pas de pub) ou même les applications de ventes privées et bien d'autres : limite j'étais prêt à  vous citer tout l'Apple Store.


 


Vu comment est géré le recyclage ça a l'air cool, propre et optimisé mais derrière je trouve c'est un peu la galère pour créer sa propre UITableViewCell avec un UIButton. J'ai eu une technique pas si mal de qqun sur ce forum pour enregistré les indices des cellules sélectionnées (dans un set) par un utilisateur. Imaginez un bouton "retweet" sur chaque cell. Mais imaginez quand vous en avez 100000 


 


Bref j'ai écris un pavé pour comprendre s'il y avait la possibilité d'approfondir ce sujet. Je ne sais pas si je suis dans le bon topic mais pour moi c'est un sujet important pour les débutants comme moi (qui ont lu la doc hein !!!). Et vu que faire une application  (hors-jeux vidéos) en se passant des UITableView c'est difficile aujourd'hui car on peut tout faire.


Réponses

  • Avec XCode tu as de très bon outils pour mesurer les performances et voir où ça ne va pas. C'est un ensemble d'optimisations.


     


    Par exemple, le data source peut avoir son importance. Imaginons que tu utilises CoreData de façon brutale, tu auras des problèmes de lag.


    Si tu fais des alloc/init coûteux et répétitifs (je pense à  NSDateFormatter), ça va te coûter cher aussi.


    Pour la partie UI, en ce qui me concerne, j'ai finalement transformé tous les éléments de ma cell en UIImage en asynchrone. De plus, lors d'un scroll, je stop à  la volée ces traitements asynchrones qui n'ont plus lieu d'être (vu que la cell a disparu).


     


    Tout ça je l'ai mesuré/expérimenté grâce aux outils XCode et cela répondait aux exigences de l'application. Probable que j'aurai utilisé d'autres techniques avec une autre appli, ou d'autres APIs sur une autre version d'iOS.


     


    Je n'ai jamais eu à  me pencher sur l'utilisation de UIButton dans un cell.


  • Am_MeAm_Me Membre
    septembre 2014 modifié #3

    J'ai donné un exemple parmis tant d'autres. Bah j'ai essayé NSZombie mais je n'ai pas compris le résultat au final ça m'a juste appris qu'il existe de très bon outils pour suivre les performance sans plus :/


     


    Mais ça m'intrigue quand même que les UITableView soit aussi "complexe" pour nous développeur. Je dis ça comme ça car juste le fait de faire une opération simple : changez la couleur d'un bouton en fonction de l'objet mis dans une cellule c'est juste casse pied car faut passer par un NSSet par exemple pour lui dire "oui tu es déjà  cliqué" par exemple ... 


     


    C'est pour ça que je voulais savoir ou en apprendre plus sur ceux que vous savez sur les ≠ facons de faire car quand je vois les applications décrite au dessus je me dis que les gas sont au top du top


  • AliGatorAliGator Membre, Modérateur
    septembre 2014 modifié #4
    Je ne suis pas sûr de comprendre la question.


    Les cells c'est la partie Vue de MVC. Grâce au recyclage t'en a que quelques unes maximum d'allouées a la fois.


    Les Tweets ou News ou autre c'est la partie Modèle du MVC. Genre dans CoreData ou autre. Rien à  voir avec le recyclage des vues.


    Si en plus tu utilises CoreData tu peux utiliser les NSFetchedResultsControllers qui ne fetchent les données que par paquets de 100 (batchSize par défaut du FRC) et gèrent automatiquement la pagination des fetch dans la base.

    Mais bon on reparlera de batchSize et de FRC quand tu auras abordé CoreData car c'est un sujet vaste et complexe (qui mérite un bouquin complet à  lui tout seul en + du Programming Guide dédié) qui ne s'explique pas en quelques posts de forum.
  • Am_MeAm_Me Membre
    septembre 2014 modifié #5

    Bah je suis d'accord c'est le modèle qui fait les requêtes mais un moment ou un autre il faut dire "Les données d'une cellule = un Tweet" et un tweet c'est un texte, une image, en plus des bouton retweet favori etc ... donc tout ça passe par Mr Recyclage non ? Donc le fait de charger le tweet à  l'indice i puis mettre en place son texte image, le fait "a t-il déjà  été retweeté par moi" "l'ai déjà  mis en favoris" ça pose problème dans l'histoire du recyclage surtout que des plateformes comme ça il y'a quasiment pas de données enregistrées en locale, voir très peu et ça fait des requêtes en continu


     


     


    EDIT : je n'avais pas vu ta nuance avec CoreData ... okay donc il y a bien un moyen de faire des choses bien belles !




  • Mais bon on reparlera de batchSize et de FRC quand tu auras abordé CoreData car c'est un sujet vaste et complexe (qui mérite un bouquin complet à  lui tout seul en + du Programming Guide dédié) qui ne s'explique pas en quelques posts de forum.




     


    Tu faiblis Ali, tu n'as pas parlé de MR  ::)

  • AliGatorAliGator Membre, Modérateur
    MR ne change rien au fait que c'est un peu tôt pour parler des FRC et des batchSize ^^ même avec MR c'est un peu tôt pour parler de CD je pense ;)
  • AliGatorAliGator Membre, Modérateur
    septembre 2014 modifié #8


    le fait de charger le tweet à  l'indice i puis mettre en place son texte image, le fait "a t-il déjà  été retweeté par moi" "l'ai déjà  mis en favoris" ça pose problème dans l'histoire du recyclage

    Je ne vois toujours pas pourquoi. C'est encore et toujoirs du MVC qu'est ce que ça a à  voir que les cells se recyclent avec le fait que tu aies à  récupérer les données de toute façon ?


    surtout que des plateformes comme ça il y'a quasiment pas de données enregistrées en locale, voir très peu et ça fait des requêtes en continu

    Heu bah encore heureux que si t'a du cache en local, même s'il est juste juste le temps de la session ou qu'il expire vite, encore heureux qu'on refait pas 50 fois la requête pour le même tweet a chaque fois qu'on en a besoin dans le modèle ! C'est même un peu le but du modèle et de la couche persistance de données !!!


    Sans même parler de CoreData ou de FRC ou de batchSize... évidemment que tu gères un cache (ne serait-ce qu'un NSURLCache au pire pour éviter de refaire les requêtes, ou mieux un NSCache pour garder le resultat parsé, ou une BDD ou un quelconque stockage des données de ton MDD.


    On est sur mobile que diable, c'est pas juste du developpement d'une appli de bureautique sur un ordi connecté en permanence à  l'ADSL non plus, les spécificités du mobile sont aussi à  gérer, c'est tout ce qui fait le challenge de tels environnements ;)

    Tu pensais quand même pas que ça serait facile et que tout ca s'apprenait seulement en quelques mois j'espère ^^
  • Am_MeAm_Me Membre
    septembre 2014 modifié #9


    Je ne vois toujours pas pourquoi. C'est encore et toujoirs du MVC qu'est ce que ça a à  voir que les cells se recyclent avec le fait que tu aies à  récupérer les données de toute façon ?

     




     


    le faite de faire cell.labelTweet.text = [[tweets objectAtIndex:indexPath.row] getText];


    le getText c'est le modele qui le fait mais la l'initialisation dans le didForRow subit le recyclage et si on scroll trop vite (enfin je crois) une autre cell peut s'attribuer le texte alors que ce n'est pas le sien.


     


     





    Tu pensais quand même pas que ça serait facile et que tout ca s'apprenait seulement en quelques mois j'espère ^^




     


    Ahaha non je ne pensais pas à  ça mais bon vu que c'est un concept très utilisé (UITableView) j'espère comprendre un jour 101% du concept. La doc est là  je sais je sais




  • le faite de faire cell.labelTweet.text = [[tweets objectAtIndex:indexPath.row] getText];


    le getText c'est le modele qui le fait mais la l'initialisation dans le didForRow subit le recyclage et si on scroll trop vite (enfin je crois) une autre cell peut s'attribuer le texte alors que ce n'est pas le sien.




     


    Pourquoi tu penses qu'une cell peut attribuer le texte d'une autre ?  


     


    Le tableView sais géré tout ça sans aucun souci. Il faut juste lui dire combien d'objets elle affiche et c'est elle qui va aller chercher l'objet qui correspond à  chaque cellule selon l'indexPath. 

  • AliGatorAliGator Membre, Modérateur
    Tant que tu ne fais pas de l'asynchrone, ou que tu ne mets pas des dispatch_async à  tout va sans réfléchir et sans savoir pourquoi tu les mets et si c'est justifié, y'a pas de problème.

    Le seul risque que tu as d'attribuer un élément à  la cell d'après, c'est si tu dispatch un block de code à  exécuter en asynchrone, et donc que le block peut s'exécuter plus tard et/ou dans un thread à  part et qu'entre temps il y a pu se passer des choses. Comme pour le cas d'une image que tu téléchargerais en tâche de fond au moment de l'affichage de la cellule, là  le téléchargement n'est pas synchrone.

    Faudrait peut-être réviser un peu les concepts de multithreading, le Concurrency Programming Guide et le Threading Programming Guide et la doc sur GCD ;)
  • Samir : Cf 



     


     


    Le seul risque que tu as d'attribuer un élément à  la cell d'après, c'est si tu dispatch un block de code à  exécuter en asynchrone, et donc que le block peut s'exécuter plus tard et/ou dans un thread à  part et qu'entre temps il y a pu se passer des choses. Comme pour le cas d'une image que tu téléchargerais en tâche de fond au moment de l'affichage de la cellule, là  le téléchargement n'est pas synchrone.

    Oui voilà  Ali par exemple comment dire si ma cellule est remise dans le queue et que je n'ai pas eu le temps de finir ma requête asynchrone : arrête tout traitement j'essayerai plus tard. 


     


    Oui je me lancerai dans le multithreading j'ai déjà  étudié le concept des thread en C et semaphore en C mais bon : je vais y jeter un bon coup d'oeil 

  • AliGatorAliGator Membre, Modérateur
    Bah tu peux par exemple implémenter des méthodes comme didMoveToWindow et si la window passée en paramètre est nil, c'est que la vue (ta cell, du coup), n'est plus dans aucune window, donc n'est plus à  l'écran, donc tu peux arrêter ta requête.

    Après, je ne sais pas si c'est une bonne idée, ça dépend de ta stratégie. Car si tu interrompt ta requête avant sa fin elle n'ira donc pas dans le cache. Ne serait-il pas + judicieux de la laisser se terminer (car y'a des chances que l'utilisateur re-scrolle dans l'autre sens sous peu et retombe sur la cellule), quand tu as la réponse, stocker la réponse dans le cache, et vérifier si ta cellule est toujours la même (ou plutôt vérifier si l'URL affectée actuellement à  la cellule est la même URL que quand tu as démarré ta requête) pour utiliser l'image ; si l'URL a changé entre temps c'est qu'il y a eu recyclage et que la cell ne correspond plus donc tu n'affectes pas l'image à  l'ImageView.

    Avantage en + de faire comme ça c'est si tu peux avoir plusieurs fois la même image sur des cellules différentes. Exemple typique : les avatars des personnes dans une conversation, ou un fil de commentaires sur un post Facebook, etc : souvent une personne va participer plusieurs fois dans la conversation, donc d'une part c'est dommage de lancer plusieurs fois la même requête pour la même URL / le même avatar juste parce que la cell a changé, si jamais l'URL de l'avatar demandé, lui, est resté le même car même après le recyclage il se trouve que tu tombes sur un commentaire de la même personne, et d'autres part ça serait dommage d'interrompre le téléchargement de l'avatar s'il n'a pas fini avant que la cellule ne soit recyclée, car il sera certainement utile / redemandé plus tard de toute façon, éventuellement sur une cellule plus loin dans la discussion / le fil de commentaires.
  • Am_MeAm_Me Membre
    septembre 2014 modifié #14

    Oui tu as raison.


    Du coup je ne vais pas faire ça comme ça. Je vais laisser comme c'est. La petite touche c'est de sauvegarder l'image dans le cache comme ça plus besoin de la recharger.


  • Tu as des lib qui font super bien tout ça, tu n'as pas à  t'occuper des l'histoire du cache,...


    Moi j'utilise souvent AFNetworking et ses catégories sur UIKit.


  • actuellement pour set une image j'utilise UIImageView+AFNetworking setImageWithUrl :) et après pour que mon image soit accessible partout je sauvegarde l'image dans mon AppDirectoy() ou HomeDirectory() plutôt :)


  • CéroceCéroce Membre, Modérateur
    septembre 2014 modifié #17
    C'est une solution bien compliquée alors que l'image a toutes les chances d'être dans le cache. Personnellement, je ne conserverais que l'URL. De plus, que se passe-t-il si l'image change sur le serveur ?
  • 1) j'ai conservé l'url. puis si l'image=nil je la recharge avec url


    2) si elle change j'ai un système de mise à  jour automatique et vu que je sauvegarde l'url no souci dans tout les cas : j'ai une information dans le xml : date du dernier changement sous le format seconde depuis 1970. Si je détecte un changement je change l'image au besoin


     


    J'ai pensé à  tout les cas ;) en tout cas pour ça je n'ai pas programmer comme un bourrin


    Au final ce n'est pas difficile de gérer si mon image = nil je lance l'url sinon je dis image = [UIImage imageNamed:] 


  • colas_colas_ Membre
    septembre 2014 modifié #19

    Je suis en train de me lancer dans le recyclage des cells et je suis dérouté. En fait, le recyclage ne suit pas le pattern MVC. En effet, on récupère la cell, mais on n'a pas de contrôleur. Je suis du coup un peu dérouté. Je voudrais créer ma cell à  l'aide du .xib, mais je voudrais aussi avoir un .m associé pour faire du code... (typiquement une méthode du type 



    - (void)confiugureCellForOptions:(MyOptions *)options

    Pourquoi Apple n'a-t-elle pas fait le choix d'associer les cell recyclées à  des contrôleurs ? Est-ce que je passe à  côté de quelque chose d'évident ? Y a-t-il un moyen classique de contrôler une custom cell (et ses dizaine de sous-vues) créée par .xib ?


     


    Merci


     


     


    EDIT 


    Je crois que j'ai compris : (cf. http://stackoverflow.com/questions/14466959/create-uicollectionviewcell-subclass-with-xib)


    - il faut créer une sous-classe de UITableViewCell


    - il faut créer un xib


    - il faut ensuite linker les deux


    - on dequeue ensuite via le xib


    - la sous-classe de cell joue le rôle du contrôleur


     


    Je persiste à  dire que ce n'est pas le MVC auquel on est habitué ! Cela permet d'économiser la création d'une classe supplémentaire, certes, mais est-ce vraiment nécessaire ?


  • CéroceCéroce Membre, Modérateur

    Disons que le système fonctionne bien tant que les cellules sont simples: c'est la data source qui nourrit directement les vues qui composent la cellule. Par exemple, si l'appli affiche une liste de plats, la data source peut récupérer le plat qui correspond à  la ligne de la table, et configure elle-même les vues de la cellule: son imageView, son textLabel.


     


    Quand les cellules deviennent complexes, ça commence à  faire beaucoup de travail pour la data source, aussi il devient intéressant de faire de la cellule un contrôleur, en créant sa propre sous-classe de UITableViewCell. La data source pourra alors se contenter de lui passer l'objet Plat, et c'est la cellule elle-même qui nourrira ses sous-vues. 


     


    Il faut savoir que cette possibilité de créer ses sous-classes de UITableViewCell n'existe que depuis iOS 5; c'est qu'entre temps les cellules sont devenues toujours plus complexes.


  • Am_MeAm_Me Membre
    septembre 2014 modifié #21

    Si ça intéresse certains j'ai fait une version "tuto" pour moi pour s'habituer à  bien créer et générer une custom UItableviewCell. (j'ai fait ça sur le logiciel Quiver je fais de la pub non intentionnelle) 


     


    Alors moi j'ai poussé la chose : dans le tuto j'ai rajouté un bouton à  ma cell donc il y a la gestion des boutons pour ceux que ça n'intéresse pas bah sautez les étapes des boutons ;)

  • RomheinRomhein Membre
    octobre 2015 modifié #22


    Bah tu peux par exemple implémenter des méthodes comme didMoveToWindow et si la window passée en paramètre est nil, c'est que la vue (ta cell, du coup), n'est plus dans aucune window, donc n'est plus à  l'écran, donc tu peux arrêter ta requête.


    Après, je ne sais pas si c'est une bonne idée, ça dépend de ta stratégie. Car si tu interrompt ta requête avant sa fin elle n'ira donc pas dans le cache. Ne serait-il pas + judicieux de la laisser se terminer (car y'a des chances que l'utilisateur re-scrolle dans l'autre sens sous peu et retombe sur la cellule), quand tu as la réponse, stocker la réponse dans le cache, et vérifier si ta cellule est toujours la même (ou plutôt vérifier si l'URL affectée actuellement à  la cellule est la même URL que quand tu as démarré ta requête) pour utiliser l'image ; si l'URL a changé entre temps c'est qu'il y a eu recyclage et que la cell ne correspond plus donc tu n'affectes pas l'image à  l'ImageView.


    Avantage en + de faire comme ça c'est si tu peux avoir plusieurs fois la même image sur des cellules différentes. Exemple typique : les avatars des personnes dans une conversation, ou un fil de commentaires sur un post Facebook, etc : souvent une personne va participer plusieurs fois dans la conversation, donc d'une part c'est dommage de lancer plusieurs fois la même requête pour la même URL / le même avatar juste parce que la cell a changé, si jamais l'URL de l'avatar demandé, lui, est resté le même car même après le recyclage il se trouve que tu tombes sur un commentaire de la même personne, et d'autres part ça serait dommage d'interrompre le téléchargement de l'avatar s'il n'a pas fini avant que la cellule ne soit recyclée, car il sera certainement utile / redemandé plus tard de toute façon, éventuellement sur une cellule plus loin dans la discussion / le fil de commentaires.




     


    J'ai justement cette problématique en ce moment ... J'ai un fil d'actu RSS et je charge les images dans des cellules qui correspondent à  une news. Bon dans une app, ce que j'aurais fait c'est une fois chargé je mets l'image en cache... Sauf que dans le centre de notif on peut monter que jusqu'à  10Mo de mémoire utilisée...


    Comment faire alors ?


    Ce que je fais pour le moment et qui n'est pas foufou en terme d'UI c'est :


    - Je ne mets rien en cache.


    - J'annule la requête dès que l'utilisateur scroll plus vite que le chargement.


     


    Aucun problème du coup en terme de mémoire, quand la connexion est rapide c'est même plutôt jolie. J'ai fais une animation sur les contraintes faisant entrée l'image de la news sur le coté.


    à‰videment le prob c'est que quand l'utilisateur revient sur ses pas, ça recharge l'image, alors qu'il l'avait déjà  chargée. Je cache un peu ça sous le tapis avec une UI qui ne présente pas de trous quand il n'y a pas d'images, et présente quand même les infos de la news (chargée au préalable, et mise en cache, car ça n'utilise rien en ressource).


     


    Je profite de ce thread pour voir si vous auriez des idées sur ce problème. C'est pas vraiment gênant, mais j'aimerai bien optimiser ça...


     


    Une idée que j'avais, c'est pendant le chargement des rss (qui s'opère quand l'utilisateur appui sur un bouton rafraichir ou en font de tache 2 fois par jours), stocker les datas des images dans ma BDD, et les afficher au fur et a mesure dans mon widget en faisant bien attention de réutiliser les UIImageView. En fait je sais pas bien ce qui prend le plus de ressource : l'objet NSData, ou l'UIImageView ?


  • CéroceCéroce Membre, Modérateur

    Note qu'il y a en fait deux niveaux de cache: le cache RAM, et le cache disque.


     


    Ce que je veux dire par là , c'est que le réseau est bien plus lent que le disque, et tu devrais déjà  profiter du cache des requêtes http (implémenté par NSURLConnection/Session). Du fait, ça m'étonne presque que tu aies le temps de voir que des images demandées il y a peu (donc dans le cache disque) n'apparaissent pas immédiatement.


    Donc, clairement, je ne vois pas d'intérêt à  stocker les images en BdD.


     


    Par ailleurs, dans une cellule, l'image est minuscule, et c'est seulement quand on demande les détails qu'on va avoir besoin de l'afficher en grand. Il est donc courant que le serveur dispose de deux versions de l'image: une pour la vignette, et une pour la grande image. Habituellement, c'est le serveur qui fait ce travail de réduction.


    Dans une de mes applis, on n'a pas le temps de voir l'image "placeholder" avec une connexion Wifi, et il n'y a vraiment qu'en Edge qu'on y prête attention.


Connectez-vous ou Inscrivez-vous pour répondre.