dessinerRect ou ne pas dessinerRect (quand faut-il utiliser drawRect / Core Graphics vs sous-vues / images et pourquoi?)

142

Pour clarifier le but de cette question: Je sais COMMENT créer des vues compliquées avec les deux sous-vues et en utilisant drawRect. J'essaie de bien comprendre le quand et le pourquoi d'utiliser l'un sur l'autre.

Je comprends également qu'il n'est pas logique d'optimiser autant à l'avance et de faire quelque chose de plus difficile avant de faire un profilage. Considérez que je suis à l'aise avec les deux méthodes et que maintenant je veux vraiment une compréhension plus profonde.

Une grande partie de ma confusion vient de l'apprentissage de la façon de rendre les performances de défilement des vues de table vraiment fluides et rapides. Bien sûr, la source originale de cette méthode provient de l' auteur derrière Twitter pour iPhone (anciennement tweetie). Fondamentalement, cela dit que pour rendre le défilement du tableau fluide, le secret est de NE PAS utiliser de sous-vues, mais de faire tout le dessin dans une seule vue personnalisée. Essentiellement, il semble que l'utilisation de nombreuses sous-vues ralentit le rendu car elles ont beaucoup de surcharge et sont constamment recomposées sur leurs vues parentes.

Pour être honnête, cela a été écrit lorsque le 3GS était assez nouveau et que les iDevices sont devenus beaucoup plus rapides depuis. Pourtant cette méthode est régulièrement suggérée sur les interwebs et ailleurs pour les tables hautes performances. En fait, c'est une méthode suggérée dans l'exemple de code de table d'Apple , a été suggérée dans plusieurs vidéos de la WWDC ( Practical Drawing for iOS Developers ) et dans de nombreux livres de programmation iOS .

Il existe même des outils géniaux pour concevoir des graphiques et générer du code Core Graphics pour eux.

Donc, au début, je suis amené à croire "il y a une raison pour laquelle Core Graphics existe. C'est RAPIDE!"

Mais dès que je pense avoir l'idée "Favoriser les graphiques de base lorsque c'est possible", je commence à voir que drawRect est souvent responsable d'une mauvaise réactivité dans une application, est extrêmement coûteux en termes de mémoire et impose vraiment le CPU. Fondamentalement, je devrais " éviter de surcharger drawRect " ( Performance de l'application iOS WWDC 2012 : graphiques et animations )

Donc je suppose que, comme tout, c'est compliqué. Peut-être que vous pouvez m'aider moi-même et aider les autres à comprendre quand et pourquoi utiliser drawRect?

Je vois quelques situations évidentes pour utiliser Core Graphics:

  1. Vous avez des données dynamiques (exemple de graphique boursier d'Apple)
  2. Vous avez un élément d'interface utilisateur flexible qui ne peut pas être exécuté avec une simple image redimensionnable
  3. Vous créez un graphique dynamique qui, une fois rendu, est utilisé à plusieurs endroits

Je vois des situations pour éviter les graphiques de base:

  1. Les propriétés de votre vue doivent être animées séparément
  2. Vous avez une hiérarchie de vues relativement petite, donc tout effort supplémentaire perçu en utilisant CG ne vaut pas le gain
  3. Vous souhaitez mettre à jour des éléments de la vue sans redessiner le tout
  4. La mise en page de vos sous-vues doit être mise à jour lorsque la taille de la vue parent change

Alors donnez vos connaissances. Dans quelles situations atteignez-vous pour drawRect / Core Graphics (qui pourrait également être accompli avec des sous-vues)? Quels facteurs vous ont conduit à cette décision? Comment / pourquoi le dessin dans une vue personnalisée est-il recommandé pour un défilement fluide des cellules de tableau, alors qu'Apple déconseille drawRect pour des raisons de performances en général? Qu'en est-il des images d'arrière-plan simples (quand les créez-vous avec CG vs en utilisant une image png redimensionnable)?

Une compréhension approfondie de ce sujet n'est peut-être pas nécessaire pour créer des applications intéressantes, mais je n'aime pas choisir entre les techniques sans pouvoir expliquer pourquoi. Mon cerveau se fâche contre moi.

Mise à jour de la question

Merci pour l'information à tous. Quelques questions de clarification ici:

  1. Si vous dessinez quelque chose avec des graphiques de base, mais que vous pouvez accomplir la même chose avec UIImageViews et un png pré-rendu, devriez-vous toujours emprunter cette voie?
  2. Une question similaire: Surtout avec des outils badass comme celui-ci , quand devriez-vous envisager de dessiner des éléments d'interface dans les graphiques de base? (Probablement lorsque l'affichage de votre élément est variable. Par exemple, un bouton avec 20 variations de couleurs différentes. D'autres cas?)
  3. Compte tenu de ma compréhension dans ma réponse ci-dessous, les mêmes gains de performances pour une cellule de tableau pourraient-ils être obtenus en capturant efficacement un instantané bitmap de votre cellule après le rendu UIView complexe lui-même, et en l'affichant tout en faisant défiler et en masquant votre vue complexe? De toute évidence, certaines pièces devraient être élaborées. Juste une pensée intéressante que j'avais.
Bob Spryn
la source

Réponses:

72

Tenez-vous-en à UIKit et aux sous-vues chaque fois que vous le pouvez. Vous pouvez être plus productif et profiter de tous les mécanismes OO qui devraient être plus faciles à maintenir. Utilisez Core Graphics lorsque vous ne pouvez pas obtenir les performances dont vous avez besoin avec UIKit, ou que vous savez qu'il serait plus compliqué d'essayer de pirater ensemble des effets de dessin dans UIKit.

Le flux de travail général devrait être de créer les vues de table avec des sous-vues. Utilisez Instruments pour mesurer la fréquence d'images sur le matériel le plus ancien que votre application prendra en charge. Si vous ne pouvez pas obtenir 60 images par seconde, accédez à CoreGraphics. Lorsque vous faites cela pendant un certain temps, vous avez une idée du moment où UIKit est probablement une perte de temps.

Alors, pourquoi Core Graphics est-il rapide?

CoreGraphics n'est pas vraiment rapide. S'il est utilisé tout le temps, vous allez probablement lentement. C'est une API de dessin riche, qui nécessite que son travail soit effectué sur le processeur, par opposition à beaucoup de travail UIKit qui est déchargé sur le GPU. Si vous deviez animer une balle se déplaçant sur l'écran, ce serait une mauvaise idée d'appeler setNeedsDisplay sur une vue 60 fois par seconde. Ainsi, si vous avez des sous-composants de votre vue qui doivent être animés individuellement, chaque composant doit être un calque séparé.

L'autre problème est que lorsque vous ne faites pas de dessin personnalisé avec drawRect, UIKit peut optimiser les vues de stock donc drawRect est un no-op, ou il peut prendre des raccourcis avec la composition. Lorsque vous remplacez drawRect, UIKit doit emprunter le chemin lent car il n'a aucune idée de ce que vous faites.

Ces deux problèmes peuvent être compensés par des avantages dans le cas des cellules de vue de tableau. Une fois que drawRect est appelé lorsqu'une vue apparaît pour la première fois à l'écran, le contenu est mis en cache et le défilement est une simple traduction effectuée par le GPU. Parce que vous avez affaire à une seule vue, plutôt qu'à une hiérarchie complexe, les optimisations drawRect d'UIKit deviennent moins importantes. Le goulot d'étranglement devient donc dans quelle mesure vous pouvez optimiser votre dessin Core Graphics.

Chaque fois que vous le pouvez, utilisez UIKit. Faites l'implémentation la plus simple qui fonctionne. Profil. Quand il y a une incitation, optimisez.

Ben Sandofsky
la source
53

La différence est que UIView et CALayer traitent essentiellement d'images fixes. Ces images sont téléchargées sur la carte graphique (si vous connaissez OpenGL, considérez une image comme une texture et un UIView / CALayer comme un polygone montrant une telle texture). Une fois qu'une image est sur le GPU, elle peut être dessinée très rapidement, et même plusieurs fois, et (avec une légère pénalité de performance) même avec des niveaux variables de transparence alpha par-dessus d'autres images.

CoreGraphics / Quartz est une API pour générer des images. Il prend un tampon de pixels (encore une fois, pensez à la texture OpenGL) et change les pixels individuels à l'intérieur. Tout cela se passe dans la RAM et sur le CPU, et ce n'est qu'une fois que Quartz est terminé, que l'image est "rincée" vers le GPU. Cet aller-retour consistant à obtenir une image du GPU, à la modifier, puis à télécharger l'image entière (ou au moins une partie relativement importante de celle-ci) vers le GPU est plutôt lent. En outre, le dessin réel que Quartz fait, bien que très rapide pour ce que vous faites, est beaucoup plus lent que ce que fait le GPU.

C'est évident, étant donné que le GPU se déplace principalement autour de pixels inchangés en gros morceaux. Quartz fait un accès aléatoire aux pixels et partage le CPU avec le réseau, l'audio, etc. De plus, si vous avez plusieurs éléments que vous dessinez en utilisant Quartz en même temps, vous devez tous les redessiner quand on change, puis télécharger le morceau entier, alors que si vous modifiez une image et que vous laissez UIViews ou CALayers la coller sur vos autres images, vous pouvez vous en sortir en téléchargeant des quantités beaucoup plus petites de données sur le GPU.

Lorsque vous n'implémentez pas -drawRect :, la plupart des vues peuvent simplement être optimisées. Ils ne contiennent aucun pixel, donc ne peuvent rien dessiner. D'autres vues, comme UIImageView, ne dessinent qu'une UIImage (qui, encore une fois, est essentiellement une référence à une texture, qui a probablement déjà été chargée sur le GPU). Donc, si vous dessinez la même UIImage 5 fois à l'aide d'une UIImageView, elle n'est téléchargée qu'une seule fois sur le GPU, puis dessinée sur l'écran à 5 emplacements différents, ce qui nous fait gagner du temps et du processeur.

Lorsque vous implémentez -drawRect :, cela provoque la création d'une nouvelle image. Vous tirez ensuite dessus sur le processeur en utilisant Quartz. Si vous dessinez une UIImage dans votre drawRect, il télécharge probablement l'image à partir du GPU, la copie dans l'image sur laquelle vous dessinez et, une fois que vous avez terminé, télécharge cette deuxième copie de l'image sur la carte graphique. Vous utilisez donc deux fois la mémoire GPU de l'appareil.

Ainsi, le moyen le plus rapide de dessiner est généralement de garder le contenu statique séparé du contenu changeant (dans des sous-classes UIViews / UIView / CALayers séparées). Chargez le contenu statique en tant qu'UIImage et dessinez-le à l'aide d'un UIImageView et placez le contenu généré dynamiquement au moment de l'exécution dans un drawRect. Si vous avez un contenu qui est dessiné à plusieurs reprises, mais qui ne change pas par lui-même (c'est-à-dire 3 icônes qui s'affichent dans le même emplacement pour indiquer un état), utilisez également UIImageView.

Une mise en garde: il y a une telle chose comme avoir trop de vues UIV. Les zones particulièrement transparentes pèsent plus lourd sur le GPU à dessiner, car elles doivent être mélangées avec d'autres pixels derrière elles lorsqu'elles sont affichées. C'est pourquoi vous pouvez marquer un UIView comme "opaque", pour indiquer au GPU qu'il peut simplement effacer tout ce qui se trouve derrière cette image.

Si vous avez un contenu qui est généré dynamiquement au moment de l'exécution mais qui reste le même pendant toute la durée de vie de l'application (par exemple, une étiquette contenant le nom d'utilisateur), il peut en fait être judicieux de simplement dessiner le tout une fois en utilisant Quartz, avec le texte, le bordure de bouton, etc., dans le cadre de l'arrière-plan. Mais c'est généralement une optimisation qui n'est pas nécessaire à moins que l'application Instruments ne vous dise différemment.

uliwitness
la source
Merci pour la réponse détaillée. Espérons que plus de gens défilent vers le bas et votent également.
Bob Spryn
J'aimerais pouvoir voter deux fois. Merci pour l'excellente rédaction!
Sea Coast of Tibet
Correction légère: Dans la plupart des cas , il n'y a pas bas la charge du GPU, mais le téléchargement est encore essentiellement la plus lente opération que vous pouvez demander à un GPU à faire, car il doit transférer des tampons de pixels de RAM système plus lent en VRAM plus rapide. OTOH une fois qu'une image est là, la déplacer implique simplement d'envoyer un nouvel ensemble de coordonnées (quelques octets) au GPU afin que iy sache où dessiner.
uliwitness
@uliwitness i parcourait votre réponse et vous avez mentionné que le fait de marquer un objet comme "opaque efface tout ce qui se cache derrière cette image". Pouvez-vous nous éclairer sur ce point?
Rikh
@Rikh Marquer un calque comme opaque signifie que a) dans la plupart des cas, le GPU ne prendra pas la peine de dessiner d'autres calques sous ce calque, enfin, au moins les parties de ceux-ci qui se trouvent sous le calque opaque. Même s'ils ont été dessinés, il n'effectuera pas réellement de fusion alpha, mais copiera simplement le contenu du calque supérieur sur l'écran (généralement les pixels entièrement transparents dans un calque opaque finiront en noir ou au moins une version opaque de leur couleur)
uliwitness
26

Je vais essayer de garder un résumé de ce que j'extrapole à partir des réponses des autres ici, et poser des questions de clarification dans une mise à jour de la question d'origine. Mais j'encourage les autres à continuer de répondre et à voter pour ceux qui ont fourni de bonnes informations.

Approche générale

Il est tout à fait clair que l'approche générale, comme Ben Sandofsky l'a mentionné dans sa réponse , devrait être "Chaque fois que vous le pouvez, utilisez UIKit. Faites l'implémentation la plus simple qui fonctionne. Profil. Quand il y a une incitation, optimisez."

Le pourquoi

  1. Il existe deux principaux goulots d'étranglement possibles dans un iDevice, le CPU et le GPU
  2. Le CPU est responsable du dessin / rendu initial d'une vue
  3. Le GPU est responsable de la majorité des animations (Core Animation), des effets de calque, de la composition, etc.
  4. UIView dispose de nombreuses optimisations, mise en cache, etc., intégrées pour gérer les hiérarchies de vues complexes
  5. Lorsque vous remplacez drawRect, vous manquez de nombreux avantages fournis par UIView, et c'est généralement plus lent que de laisser UIView gérer le rendu.

Dessiner le contenu des cellules dans un UIView plat peut considérablement améliorer votre FPS sur les tableaux défilants.

Comme je l'ai dit ci-dessus, le CPU et le GPU sont deux goulots d'étranglement possibles. Comme ils gèrent généralement des choses différentes, vous devez faire attention à quel goulot d'étranglement vous vous heurtez. Dans le cas des tableaux de défilement, ce n'est pas que Core Graphics dessine plus rapidement, et c'est pourquoi il peut grandement améliorer votre FPS.

En fait, Core Graphics peut très bien être plus lent qu'une hiérarchie UIView imbriquée pour le rendu initial. Cependant, il semble que la raison typique du défilement saccadé soit que vous goulotez le GPU, vous devez donc résoudre ce problème.

Pourquoi le remplacement de drawRect (à l'aide de graphiques de base) peut aider le défilement du tableau:

D'après ce que je comprends, le GPU n'est pas responsable du rendu initial des vues, mais se voit remettre des textures, ou des bitmaps, parfois avec certaines propriétés de calque, après leur rendu. Il est alors responsable de la composition des bitmaps, du rendu de tous ces effets de calque et de la majorité de l'animation (Core Animation).

Dans le cas des cellules de vue de tableau, le GPU peut être goulot d'étranglement avec des hiérarchies de vues complexes, car au lieu d'animer un bitmap, il anime la vue parente, effectue des calculs de disposition de sous-vue, rend les effets de calque et compose toutes les sous-vues. Ainsi, au lieu d'animer un bitmap, il est responsable de la relation d'un groupe de bitmaps, et de la façon dont ils interagissent, pour la même zone de pixels.

Donc, en résumé, la raison pour laquelle dessiner votre cellule dans une vue avec des graphiques de base peut accélérer le défilement de votre tableau n'est PAS parce qu'elle dessine plus rapidement, mais parce que cela réduit la charge sur le GPU, ce qui est le goulot d'étranglement qui vous pose problème dans ce scénario particulier. .

Bob Spryn
la source
1
Attention cependant. Les GPU recomposent efficacement l'ensemble de l'arborescence des couches pour chaque image. Le déplacement d'une couche est donc effectivement un coût nul. Si vous créez un seul grand calque, le déplacement d'un seul calque est plus rapide que le déplacement de plusieurs. Déplacer quelque chose à l' intérieur de cette couche implique soudainement le processeur et a un coût. Étant donné que le contenu des cellules ne change généralement pas beaucoup (uniquement en réponse à des actions utilisateur relativement rares), l'utilisation de moins de vues accélère les choses. Mais faire une grande vue avec toutes les cellules serait une mauvaise idée ™.
uliwitness
6

Je suis un développeur de jeux et je posais les mêmes questions quand mon ami m'a dit que ma hiérarchie de vues basée sur UIImageView allait ralentir mon jeu et le rendre terrible. J'ai ensuite recherché tout ce que je pouvais trouver pour savoir s'il fallait utiliser UIViews, CoreGraphics, OpenGL ou quelque chose de tiers comme Cocos2D. La réponse constante que j'ai obtenue d'amis, d'enseignants et d'ingénieurs Apple à la WWDC était qu'il n'y aura pas beaucoup de différence à la fin parce qu'à un certain niveau, ils font tous la même chose . Les options de niveau supérieur telles que UIViews reposent sur les options de niveau inférieur telles que CoreGraphics et OpenGL, mais elles sont simplement enveloppées dans du code pour vous faciliter l'utilisation.

N'utilisez pas CoreGraphics si vous allez simplement réécrire l'UIView. Cependant, vous pouvez gagner de la vitesse en utilisant CoreGraphics, à condition de faire tout votre dessin dans une seule vue, mais cela en vaut- il vraiment la peine ? La réponse que j'ai trouvée est généralement non. Quand j'ai commencé mon jeu, je travaillais avec l'iPhone 3G. Au fur et à mesure que mon jeu devenait de plus en plus complexe, j'ai commencé à voir un certain décalage, mais avec les nouveaux appareils, c'était complètement imperceptible. Maintenant, j'ai beaucoup d'action en cours, et le seul décalage semble être une baisse de 1 à 3 fps lorsque vous jouez au niveau le plus complexe sur un iPhone 4.

J'ai quand même décidé d'utiliser Instruments pour trouver les fonctions qui prenaient le plus de temps. J'ai trouvé que les problèmes n'étaient pas liés à mon utilisation d'UIViews . Au lieu de cela, il appelait à plusieurs reprises CGRectMake pour certains calculs de détection de collision et chargeait les fichiers image et audio séparément pour certaines classes qui utilisent les mêmes images, plutôt que de les faire tirer d'une classe de stockage centrale.

Donc, en fin de compte, vous pourrez peut-être obtenir un léger gain en utilisant CoreGraphics, mais généralement cela ne vaut pas la peine ou peut ne pas avoir d'effet du tout. La seule fois où j'utilise CoreGraphics, c'est lorsque je dessine des formes géométriques plutôt que du texte et des images.

WolfLink
la source
8
Je pense que vous confondez peut-être Core Animation avec Core Graphics. Core Graphics est un dessin basé sur un logiciel très lent et n'est pas utilisé pour dessiner des vues UIV normales, sauf si vous remplacez la méthode drawRect. Core Animation est une accélération matérielle, rapide et responsable de la plupart de ce que vous voyez à l'écran.
Nick Lockwood du