UITableView Rapide
Bonjour,
je viens vers vous car j'ai un problème d'optimisation sur ma TableView.
Je suis actuellement sur un projet ou je doit afficher une tableview dont chaque cellule contiendra une image (de la taille de l'écran iPhone).
Ce sont des images JPEG qui font entre 600 et 800ko.
Dans une tableview il peux y avoir plus de 130 cellules.
Je ne sais pas trop comment m'y prendre pour que la tableview soit réactive.
Actuellement j'ai essayé deux méthodes.
La première, celle que j'utilise d'habitude, donne de très mauvais résultat. Elle ressemble à ça:
Avec cette première méthode, j'utilise un chargement asynchrone des images. J'associe à chaque cellule l'indexpath courant. Pour que dans le block asynchrone je puisse vérifié si le chargement de l'image est toujours d'actualité (si la cellule n'a pas changé d'image).
Le defilement de la tableview est trop saccadé (quand on veut allé vite sur la tableview).
J'ai donc pensé à un système que je n'arrive pas à mettre en place.
J'en arrive à ce code.
Avec cette technique la tableview est vraiment très réactive. Mais je n'arrive pas exactement à ce que je voulais, car le xxx n'est jamais pris en compte, l'image se recharge seulement quand la tableview arrete de bougé. C'est à dire que pendant le scroll, l'image ne se charge pas, mais elle se charge quand la tableview arrive à la fin de son scroll.
Ce qui est assez genant.
Une idée pour accélérer ma tableview ?
En améliorant ma deuxième technique ou quelque chose de complètement different ?
je viens vers vous car j'ai un problème d'optimisation sur ma TableView.
Je suis actuellement sur un projet ou je doit afficher une tableview dont chaque cellule contiendra une image (de la taille de l'écran iPhone).
Ce sont des images JPEG qui font entre 600 et 800ko.
Dans une tableview il peux y avoir plus de 130 cellules.
Je ne sais pas trop comment m'y prendre pour que la tableview soit réactive.
Actuellement j'ai essayé deux méthodes.
La première, celle que j'utilise d'habitude, donne de très mauvais résultat. Elle ressemble à ça:
<br />
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath<br />
{<br />
static NSString *CellIdentifier = @"Cell";<br />
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];<br />
<br />
if (cell == nil) {<br />
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault<br />
reuseIdentifier:CellIdentifier];<br />
UIImageView *imageView = [[[UIImageView alloc] initWithFrame:CGRectMake(0,0,<br />
320,<br />
537)] autorelease];<br />
imageView.tag = 770;<br />
[cell.contentView addSubview:imageView];<br />
[cell autorelease];<br />
}<br />
<br />
NSString *stringImage = @"07.0";<br />
stringImage = [stringImage stringByAppendingFormat:@"%d.jpg", indexPath.row];<br />
UIImageView *imageView = (UIImageView *)[cell.contentView viewWithTag:770]; <br />
imageView.image = nil;<br />
<br />
objc_setAssociatedObject(cell, kIndexPathAssociationKey, indexPath, OBJC_ASSOCIATION_RETAIN);<br />
dispatch_async(dispatch_get_main_queue(), ^{<br />
NSIndexPath *oldIndexPath = objc_getAssociatedObject(cell, kIndexPathAssociationKey);<br />
if ([oldIndexPath isEqual:indexPath]) {<br />
imageView.image = [UIImage imageNamed:stringImage];<br />
}<br />
});<br />
return cell;<br />
}<br />
Avec cette première méthode, j'utilise un chargement asynchrone des images. J'associe à chaque cellule l'indexpath courant. Pour que dans le block asynchrone je puisse vérifié si le chargement de l'image est toujours d'actualité (si la cellule n'a pas changé d'image).
Le defilement de la tableview est trop saccadé (quand on veut allé vite sur la tableview).
J'ai donc pensé à un système que je n'arrive pas à mettre en place.
- On charge l'image, seulement si la cellule est visible depuis x ms. Avec x un nombre assez petit pour ne pas voir la saccade, mais assez grand pour faire en sorte que la tableview soit réactive.
J'en arrive à ce code.
<br />
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath<br />
{<br />
static NSString *CellIdentifier = @"Cell";<br />
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];<br />
<br />
if (cell == nil) {<br />
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault<br />
reuseIdentifier:CellIdentifier];<br />
UIImageView *imageView = [[[UIImageView alloc] initWithFrame:CGRectMake(0,0,<br />
320,<br />
537)] autorelease];<br />
imageView.tag = 770;<br />
[cell.contentView addSubview:imageView];<br />
[cell autorelease];<br />
}<br />
<br />
NSString *stringImage = @"07.0";<br />
stringImage = [stringImage stringByAppendingFormat:@"%d.jpg", indexPath.row];<br />
UIImageView *imageView = (UIImageView *)[cell.contentView viewWithTag:770]; <br />
imageView.image = nil;<br />
<br />
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:<br />
cell, @"cell",<br />
indexPath, @"indexPath",<br />
imageView, @"imageView",<br />
stringImage, @"stringImage",<br />
nil];<br />
<br />
objc_setAssociatedObject(cell, kIndexPathAssociationKey, indexPath, OBJC_ASSOCIATION_RETAIN);<br />
<br />
float xxx = 0.500 /// Le xxx est ici<br />
[NSTimer scheduledTimerWithTimeInterval:xxx<br />
target:self selector:@selector(loadImage:) userInfo:userInfo repeats:false];<br />
return cell;<br />
}<br />
- (void)loadImage:(NSTimer *)timer<br />
{<br />
NSDictionary *userInfo = timer.userInfo;<br />
UIImageView *imageView = [userInfo objectForKey:@"imageView"];<br />
NSIndexPath *indexPath = [userInfo objectForKey:@"indexPath"];<br />
NSString *stringImage = [userInfo objectForKey:@"stringImage"];<br />
UITableViewCell *cell = [userInfo objectForKey:@"cell"];<br />
<br />
<br />
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);<br />
dispatch_async(queue, ^{<br />
dispatch_async(dispatch_get_main_queue(), ^{<br />
NSIndexPath *oldIndexPath = objc_getAssociatedObject(cell, kIndexPathAssociationKey);<br />
if ([oldIndexPath isEqual:indexPath]) {<br />
imageView.image = [UIImage imageNamed:stringImage];<br />
}<br />
});<br />
});<br />
}<br />
Avec cette technique la tableview est vraiment très réactive. Mais je n'arrive pas exactement à ce que je voulais, car le xxx n'est jamais pris en compte, l'image se recharge seulement quand la tableview arrete de bougé. C'est à dire que pendant le scroll, l'image ne se charge pas, mais elle se charge quand la tableview arrive à la fin de son scroll.
Ce qui est assez genant.
Une idée pour accélérer ma tableview ?
En améliorant ma deuxième technique ou quelque chose de complètement different ?
Mots clés:
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
L'idée serait de redimensionner par le code tes images pour qu'elles fassent la taille de ton imageView.
Cette opération ne serait à faire qu'une fois au premier chargement de ton image et après tu la stocke en cache dans un mutableDictionary (que tu peux vider dans le didReceiveMemoryWarning).
Ensuite, je ne suis vraiment pas convaincu de ton utilisation de Grand Central Dispatch dans ton second exemple où tu fais un dispatch dans une queue à part pour après refaire de suite le dispatch dans la main queue.
ça marche pas de faire directement le dispatch sur la main queue ? En tout cas tu peux faire le redimensionnement de l'image dans ce block.
Après, ça joue ptet pas énormément sur les perfs, mais à quoi te sert ton associatedObject ? j'ai l'impression que c'est juste pour t'assurer que c'est bien la bonne cell que tu modifie et qu'elle n'ait pas été reuse entre temps.
Mais pour ça tu as la methode cellForRowAtIndexPath: de ta tableView qui te retournera toujours la bonne cell (et nil si t'as row n'est pas visible il me semble).
Enfin, tu devrais passer par une sous-classes de UITableViewCell, ce sera plus pratique que de passer par les tags et des dictionnaires.
Si je résume, perso je ferais un truc du genre
Je me suis mal exprimer, c'est la cellule qui fait la taille de l'écran, les images sont encore plus grandes (elles ont la resolution d'un ipad)
Je vais essayé de suite et je te dit ce que ça donne. Mais j'ai peu que le redimensionnement prenne du temps, et que ça ne change pas le problème de fluidité.
En faite la tableview peut contenir jusqu'à 150 cellules, et les cellules sont en général afficher qu'une seule fois. Soit en scrollant au fur et à mésure, soit en passant directement à la cellule X via des boutons prévu à cet effets.
Moi non plus je ne suis pas convaincu. A vrai dire, j'ai un peu joué avec tout ce que je pouvais pour arrivé à un résultat correct. Quand je ne met qu'un seul dispatch sur une queue autre que la main queue, la cellule ne se réaffiche pas (je pense que c'est normal, car les modification de l'ui doit être fait seulement sur le main queue).
Quand je met directement la dispatch sur la main queue, le résultat est identique c'est à dire, l'image est afficher seulement quand la tableview est immobile.
Oui c'est pour m'assurer que la cell n'a pas été reuse entre temps. Je ne connaissais pas spécialement la méthode cellForRowAtIndexPath. Mais pour le associatedObject, j'avais lu sur un article que c'étais une bonne méthode.
http://blog.slauncha...ble-view-cells/
Cela va de soit, mais le code que j'ai donnée, et plus un prototype technique pour m'assurer que je pars dans la bonne direction.
Cordialement.
Je pense, qu'avant de faire de l'asynchrone, tu devrais d'abord optimiser l'affichage de ces images en (comme l'a suggéré Jegnux) sous classant ta cell.
Tu peux par exemple réduire le blending des view de ta cell ce qui peut nettement améliorer les performances.
Ce n'est qu'après que tu pourras introduire éventuellement de l'asynchrone si tu vois que les choses ne s'améliorent pas.
Par contre j'ai compris pourquoi il faisait un premier dispatch dans une queue à part, puis un autre dans la main.
Sauf que toi, tu fais pas comme lui et du coup tu perds l'avantage qu'il mentionne : l'ouverture du fichier en background. Alors que toi tu fais ton imageNamed: dans le block qui est appelé sur la main.
Pour le redimensionnement et le cache c'est comme tu veux. Mais le cache en tout cas te permettrais de pas avoir a ré-ouvrir le fichier à chaque passage sur ta cell.
Voici le code que je t'avais donné un peu modifié. Tente le /wink.png' class='bbc_emoticon' alt=';)' />
D'accord, je passe tout de suite à une sous classe.
Je veux bien optimier l'affichage des images (c'est meme pour ça que je suis la). Mais je ne sais pas trop comment faire.
Mais qu'est ce le blending des view de ma cell ?
[font=helvetica, arial, sans-serif]Il est dans le depot github donner en lien à la fin de l'article.[/font]
https://github.com/SlaunchaMan/GCDExample/blob/master/GCDExample/RootViewController.m#L139
[font=helvetica, arial, sans-serif]Je viens aussi de comprendre le truc.[/font]
[font=helvetica, arial, sans-serif]J'essaye de suite et je vous tiens au courant.[/font]
Ah exact. Mais je suis pas plus convaincu de sa pertinence par rapport à l'utilisation de -cellForRowAtIndexPath:
Ils en pensent quoi les autres ?
Qu'est ce qu'elle fait d'autre ton application ? Car la dans le code que j'ai mis, en théorie y'a pas grand chose qui bloque le main thread, donc ça devrait scroller sans accoups. Mais tu fais peut-être autre chose dans ton code qui bloque le main thread ?
Pour répondre à la question: c'est pendant le scroll que sa saccade. En effet ça peut rester plus d'une seconde bloqué sur la meme cellule.
Je pense que c'est cette ligne qui bloque le main thread
Et ça devrait rejoindre ce que disait Kubernan en disant d'optimiser d'abord l'affichage des images. Car mes images font en moyenne 800ko, c'est surement l'affichage de l'image qui met du temps.
Synchrone ou pas... ton image il faut de toute façon l'afficher. Ca prend... un certain temps :-)
Lorsque tu veux afficher une image le système graphique effectue pas mal d'opérations, notamment s'il y a nécessité de "mélanger" le rendu de certaines parties.
Tu peux réduire ces calculs en utilisant par exemple des vues opaques (c.f. propriété opaque de UIView : If set to YES, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance).
Vérifie également la propriété Alpha de tes images. Si tu as la possibilité de la supprimer je crois que ça peut avoir un impact.
Tu peux aussi aplatir la hiérarchie de tes vues. C'est ce que je fais pour certaines de mes cells qui affichent pas mal d'image : plutôt que de multiplier les UIImageView, je dessine (via drawRect:) toutes mes images dans une seule vue par la méthode drawAtPoint: de UIImage.
J'imagine bien que ça doit prendre un temps incompressible pour pouvoir afficher les images. Mais mon idée était d'afficher ces images seulement si il y a necessité, et cet necessité était définie par un temps d'affichage de la cellule. (chose que je n'arrive pas à mettre en place).
Mais normalement c'est juste des images simple. pas de mélange.
J'ai passé le opaque de ma cellule ainsi que de mon uiimageview à false. La difference se voit, mais sa reste encore beaucoup trop saccadé.
Mes images sont des images jpg donc elle non pas de couches alpha.
J'imagine que ça ne se fait pas facilement avec une UITableView ?
Le client possède un livre qu'il veut mettre sur iphone/ipad. Mais il ne possède qu'une version imagé de son livre.
Donc c'est pour un lecteur de livre.[/font]
C'est pas compliqué du tout, je te conseille d'aller étudier l'exemple de code intitulé "AdvancedTableViewCells" dans les sample code Apple où tu as quelques exemples d'utilisation de sous classes de cell (dont un avec le drawAtPoint: dont je t'ai parlé).
Il n'y a pas de solutions définitives soit dit en passant, il est parfois plus utile de laisser UIImageView faire. Il faut que tu regardes avec Instrument à quel moment précis ton code consume le plus de temps.
130 images de 800 Ko ça fait 101 Mo ! C'est beaucoup, vraiment beaucoup.. Vous avez pensé à utiliser une solution d'OCR pour transformer les images en texte, du moins en partie ?
Pense à regarder la session 104 des vidéos Apple de la WWDC 2010, sur le site de développement. On y explique comment réaliser le système de scrolling de l'application Photo.
On ne peux pas faire une solution d'OCR car la mise en page du texte est très importantes.
Pour être utilisable un ouvrage doit être adapté au format de lecture, pas simplement porté d'un support à un autre.
La mise en page de ce livre est ce qui fait sa spécificité, et les utilisateurs iPhone/iPad veulent retrouver la même mise en page que le support papier.
La seul contrainte c'est la contrainte de poids, l'application pèse assez lourd, mais je ne vois pas comment faire autrement. Mais le faite qu'elle ressemble à la version papier est voulu.
image
Le mieux serait de faire une liseuse normale non ? C'est à dire comme l'application photos : scroll horizontal uniquement (où vertical si tu y tiens vraiment, c'est possible aussi) et on ne charge que les images en n-1,n et n+1. Pas de scroll rapide mais juste un scroll de page en page.
Ainsi on peut aussi optimiser la mémoire bien release les images qui ne sont pas en n-1,n et n+1 (à ce sujet, n'utilise pas imageNamed: du coup car il met l'image en cache, mais c'est pas forcément utile ici donc fait avec initWithContentsOfFile: )
Fin bref, du coup je pense pas que la tableView soit vraiment adapter à ce que tu veux.
En affichant tout sous forme de textes, tu passeras de 200 mo à un ou deux Mo seulement ! Une paille, quoi .. Sans parler d'une colossale amélioration de la fluidité ! Et tu profiteras de la qualité d'affichage du Retina Display, sans rien modifier au code !
Justement c'est pour cela que j'utilise la tableview. Pour profiter des reuseCell sans avoir à le recoder. A partir du moment ou j'utilise un défilement vertical (à la demande du client), je trouve la tableview adaptée pour ne pas tout réinventer. Vu que ce n'est pas un livre de lecture, il faut pouvoir naviguer rapidement au scroll sur les differentes pages pour recherche le passage qui nous interesse.
@Draken
http://dl.dropbox.co...001_recadre.jpg
Regarde à ce niveau là de l'image par exemple. Je ne vois pas comment représenté cette suite de mot en texte pure. Ni comment représenter une lettre qui en contient plusieurs (la quatrième lettre en partant de la droite contient un mot complet). Et à la fin de la ligne il y a des mots qui sont écrit sur plusieurs ligne.
Je suis d'accord, c'est sur que sa serait mieux d'avoir tout en vectoriel, ou d'avoir le texte brut. Mais c'est quelque chose que je n'est pas, et je ne sais pas comment représenté exactement la meme mise en page en utilisant un OCR
Je viens de tomber sur un cas ou l'utilisation de cellForRowAtIndexPath ne marche pas dans l'utilisation que l'on fait.
En effet parfois on rentre à l'interieur du dispatch_async avant meme de faire le return cell,(donc dès fois c'est synchrone)
Et à l'interieur du dispatch_async on appel la méthode cellForRowAtIndexPath qui va forcement renvoyé nil car le return Cell n'a pas été fait. Pour l'instant chez moi ce cas n'arrive qu'a la première cellule de la tableview, et qu'au premier chargement.
Il y a surement un moyen de ruser. Mais les deux cas ne sont pas directement interchangeable.
A priori ton ouvrage est un ouvrage religieux traditionnel. Il en existe peut être des versions libres de droits sous forme numérique. Cela pourrais t'aider de les étudier.
http://dl.dropbox.com/u/9704388/forum/07.053.jpg
De plus je ne vois pas comment rendre ce genre de page facilement. Avec traduction mot à mot.
Je le répète, je suis entièrement d'accord avec toi Draken. Mais sa me demanderait un gros travail de refaire un siddour entièrement. (ce genre de livre de prière s'appelle un siddour). Et ce n'est pas dans le cadre de ce projet.
Ce qui prend le plus de temps c'est surtout de lire le fichier image, et le charger en RAM. L'affichage prend également du temps, bien qu'un peu moins.
Si tu veux pouvoir défiler rapidement, il faut que dans ton cellForRowAtIndexPath tu affectes une image en basse définition, lue depuis un fichier image bien moins lourd donc bien moins long à charger. Ou préchargée dans un tableau de UIImage que tu gardes de côté, bien que ça ferait garder quasi tout le livre (même si tu le remplis au fur et à mesure) en RAM et donc risquerait là aussi de faire beaucoup.
Et quand la scrollView décélère (tu as une méthode de delegate qui te prévient de ça) voire s'arrête de scroller, tu charges l'image High Def à la place de l'image LowDev (utilise la propriété visibleCells de UITableView pour ça)
Bref dans tous les cas il te faudra faire des compromis : tes images sont grosses donc longues à charger, tu ne peux pas les charger en un temps records pour fluidifier ton scrolling. Et tu ne peux pas toutes les précharger ne mémoire, car elles sont trop grosses et ça exploserait la RAM. Donc il faut que tu embarques dans ton bundle des versions basse définition de tes images* en plus des images HD.
* note que si tes images sont toutes comme celles que tu as montrée plus haut, donc que du texte mais en plus qu'en noir et blanc ou niveau de gris, il y a certainement moyen d'optimiser la taille même en gardant des images sans parler d'OCR...?
EDIT : Grilled par Ali, comme toujours !
Je vais commencé par faire un traitement par lot. Et utilisé la technique des photo lowres de Aligator voir ce que ça donne.
J'ai essayé pas mal de reglages avec photoshop mais je n'arrive pas à avoir des images plus legère que la version jpg.