Conseils et outils pour optimiser les applications Android
Publié: 2022-03-11Les appareils Android ont beaucoup de cœurs, donc écrire des applications fluides est une tâche simple pour n'importe qui, n'est-ce pas ? Tort. Comme tout sur Android peut être fait de différentes manières, choisir la meilleure option peut être difficile. Si vous voulez choisir la méthode la plus efficace, vous devez savoir ce qui se passe sous le capot. Heureusement, vous n'avez pas à vous fier à vos sentiments ou à votre odorat, car il existe de nombreux outils qui peuvent vous aider à trouver les goulots d'étranglement en mesurant et en décrivant ce qui se passe. Des applications correctement optimisées et fluides améliorent considérablement l'expérience utilisateur et consomment également moins de batterie.
Voyons d'abord quelques chiffres pour considérer l'importance réelle de l'optimisation. Selon un article de Nimbledroid, 86 % des utilisateurs (dont moi) ont désinstallé des applications après les avoir utilisées une seule fois en raison de mauvaises performances. Si vous chargez du contenu, vous avez moins de 11 secondes pour le montrer à l'utilisateur. Seul un utilisateur sur trois vous donnera plus de temps. Vous pourriez également recevoir de nombreuses critiques négatives sur Google Play à cause de cela.
La première chose que chaque utilisateur remarque encore et encore est le temps de démarrage de l'application. Selon un autre article de Nimbledroid, sur les 100 meilleures applications, 40 démarrent en moins de 2 secondes et 70 démarrent en moins de 3 secondes. Donc, si possible, vous devez généralement afficher du contenu dès que possible et retarder un peu les vérifications des antécédents et les mises à jour.
Rappelez-vous toujours que l'optimisation prématurée est la racine de tous les maux. Vous ne devriez pas non plus perdre trop de temps avec la micro-optimisation. Vous verrez le plus grand avantage de l'optimisation du code qui s'exécute souvent. Par exemple, cela inclut la fonction onDraw()
, qui exécute chaque image, idéalement 60 fois par seconde. Le dessin est l'opération la plus lente qui existe, alors essayez de ne redessiner que ce que vous devez faire. Plus d'informations à ce sujet viendront plus tard.
Conseils de performances
Assez de théorie, voici une liste de certaines des choses que vous devriez considérer si la performance compte pour vous.
1. Chaîne contre StringBuilder
Supposons que vous ayez une chaîne et que, pour une raison quelconque, vous souhaitiez lui ajouter plus de chaînes 10 000 fois. Le code pourrait ressembler à ceci.
String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }
Vous pouvez voir sur les moniteurs Android Studio à quel point certaines concaténations de chaînes peuvent être inefficaces. Il y a des tonnes de Garbage Collections (GC) en cours.
Cette opération prend environ 8 secondes sur mon assez bon appareil, qui a Android 5.1.1. Le moyen le plus efficace d'atteindre le même objectif consiste à utiliser un StringBuilder, comme celui-ci.
StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();
Sur le même appareil, cela se produit presque instantanément, en moins de 5 ms. Les visualisations du processeur et de la mémoire sont presque totalement plates, vous pouvez donc imaginer l'ampleur de cette amélioration. Notez cependant que pour obtenir cette différence, nous avons dû ajouter 10 000 chaînes, ce que vous ne faites probablement pas souvent. Donc, si vous n'ajoutez que quelques chaînes une fois, vous ne verrez aucune amélioration. Au fait, si vous faites :
String string = "hello" + " world";
Il est converti en interne en StringBuilder, il fonctionnera donc très bien.
Vous vous demandez peut-être pourquoi la concaténation de Strings est-elle si lente ? Cela est dû au fait que les chaînes sont immuables, donc une fois qu'elles sont créées, elles ne peuvent pas être modifiées. Même si vous pensez modifier la valeur d'une chaîne, vous créez en fait une nouvelle chaîne avec la nouvelle valeur. Dans un exemple comme :
String myString = "hello"; myString += " world";
Ce que vous obtiendrez en mémoire n'est pas 1 String "hello world", mais en fait 2 Strings. La chaîne myString contiendra "hello world", comme vous vous en doutez. Cependant, la chaîne d'origine avec la valeur "hello" est toujours active, sans aucune référence à celle-ci, en attente d'être ramassée. C'est aussi la raison pour laquelle vous devriez stocker les mots de passe dans un tableau de caractères au lieu d'une chaîne. Si vous stockez un mot de passe sous forme de chaîne, il restera dans la mémoire dans un format lisible par l'homme jusqu'au prochain GC pendant une durée imprévisible. De retour à l'immuabilité décrite ci-dessus, la chaîne restera dans la mémoire même si vous lui affectez une autre valeur après l'avoir utilisée. Cependant, si vous videz le tableau de caractères après avoir utilisé le mot de passe, il disparaîtra de partout.
2. Choisir le bon type de données
Avant de commencer à écrire du code, vous devez décider des types de données que vous utiliserez pour votre collection. Par exemple, faut-il utiliser un Vector
ou un ArrayList
? Eh bien, cela dépend de votre cas d'utilisation. Si vous avez besoin d'une collection thread-safe, qui n'autorisera qu'un seul thread à la fois à travailler avec, vous devez choisir un Vector
, car il est synchronisé. Dans d'autres cas, vous devriez probablement vous en tenir à un ArrayList
, à moins que vous n'ayez vraiment une raison spécifique d'utiliser des vecteurs.
Que diriez-vous du cas où vous souhaitez une collection avec des objets uniques ? Eh bien, vous devriez probablement choisir un Set
. Ils ne peuvent pas contenir de doublons par conception, vous n'aurez donc pas à vous en occuper vous-même. Il existe plusieurs types d'ensembles, alors choisissez celui qui correspond à votre cas d'utilisation. Pour un simple groupe d'éléments uniques, vous pouvez utiliser un HashSet
. Si vous souhaitez conserver l'ordre des éléments dans lesquels ils ont été insérés, choisissez un LinkedHashSet
. Un TreeSet
trie les éléments automatiquement, vous n'aurez donc pas à appeler de méthodes de tri dessus. Il doit également trier les éléments efficacement, sans que vous ayez à penser à des algorithmes de tri.
— Les 5 règles de programmation de Rob Pike
Le tri des entiers ou des chaînes est assez simple. Cependant, que se passe-t-il si vous souhaitez trier une classe selon une propriété ? Disons que vous écrivez une liste de repas que vous mangez et stockez leurs noms et horodatages. Comment classeriez-vous les repas par horodatage, du plus bas au plus élevé ? Heureusement, c'est assez simple. Il suffit d'implémenter l'interface Comparable
dans la classe Meal
et de surcharger la fonction compareTo()
. Pour trier les repas de l'horodatage le plus bas au plus élevé, nous pourrions écrire quelque chose comme ça.
@Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }
3. Mises à jour de l'emplacement
Il existe de nombreuses applications qui collectent la position de l'utilisateur. Vous devez utiliser l'API Google Location Services à cette fin, qui contient de nombreuses fonctions utiles. Il y a un article séparé sur son utilisation, donc je ne le répéterai pas.
Je voudrais juste souligner quelques points importants du point de vue de la performance.
Tout d'abord, utilisez uniquement l'emplacement le plus précis dont vous avez besoin. Par exemple, si vous faites des prévisions météorologiques, vous n'avez pas besoin de l'emplacement le plus précis. Obtenir juste une zone très approximative basée sur le réseau est plus rapide et plus économe en batterie. Vous pouvez y parvenir en définissant la priorité sur LocationRequest.PRIORITY_LOW_POWER
.
Vous pouvez également utiliser une fonction de LocationRequest
appelée setSmallestDisplacement()
. Si vous définissez ce paramètre en mètres, votre application ne sera pas avertie du changement d'emplacement s'il était inférieur à la valeur donnée. Par exemple, si vous avez une carte avec des restaurants à proximité autour de vous et que vous définissez le plus petit déplacement à 20 mètres, l'application ne fera pas de demandes de vérification des restaurants si l'utilisateur se promène simplement dans une pièce. Les demandes seraient inutiles, car il n'y aurait de toute façon pas de nouveau restaurant à proximité.
La deuxième règle consiste à demander des mises à jour de localisation aussi souvent que vous en avez besoin. C'est assez explicite. Si vous construisez vraiment cette application de prévisions météo, vous n'avez pas besoin de demander l'emplacement toutes les quelques secondes, car vous n'avez probablement pas de prévisions aussi précises (contactez-moi si vous en avez). Vous pouvez utiliser la fonction setInterval()
pour définir l'intervalle requis dans lequel l'appareil mettra à jour votre application à propos de l'emplacement. Si plusieurs applications continuent de demander l'emplacement de l'utilisateur, chaque application sera avertie à chaque nouvelle mise à jour d'emplacement, même si vous avez défini un setInterval()
plus élevé. Pour éviter que votre application ne soit notifiée trop souvent, assurez-vous de toujours définir un intervalle de mise à jour le plus rapide avec setFastestInterval()
.
Et enfin, la troisième règle demande des mises à jour de localisation uniquement si vous en avez besoin. Si vous affichez des objets à proximité sur la carte toutes les x secondes et que l'application passe en arrière-plan, vous n'avez pas besoin de connaître le nouvel emplacement. Il n'y a aucune raison de mettre à jour la carte si l'utilisateur ne peut pas la voir de toute façon. Assurez-vous d'arrêter d'écouter les mises à jour de localisation le cas échéant, de préférence dans onPause()
. Vous pouvez ensuite reprendre les mises à jour dans onResume()
.
4. Demandes de réseau
Il y a de fortes chances que votre application utilise Internet pour télécharger ou télécharger des données. Si c'est le cas, vous avez plusieurs raisons de prêter attention au traitement des requêtes réseau. L'un d'eux est les données mobiles, qui sont très limitées pour beaucoup de gens et vous ne devriez pas les gaspiller.
Le second est la batterie. Les réseaux WiFi et mobiles peuvent en consommer beaucoup s'ils sont trop utilisés. Disons que vous voulez télécharger 1 ko. Pour faire une demande de réseau, vous devez réveiller la radio cellulaire ou WiFi, puis vous pouvez télécharger vos données. Cependant, la radio ne s'endormira pas immédiatement après l'opération. Il restera dans un état assez actif pendant environ 20 à 40 secondes supplémentaires, selon votre appareil et votre opérateur.
Alors, que pouvez-vous faire à ce sujet? Grouper. Pour éviter de réveiller la radio toutes les deux secondes, prérécupérez les éléments dont l'utilisateur pourrait avoir besoin dans les minutes à venir. La bonne méthode de traitement par lots est très dynamique en fonction de votre application, mais si cela est possible, vous devez télécharger les données dont l'utilisateur pourrait avoir besoin dans les 3 à 4 prochaines minutes. On peut également modifier les paramètres du lot en fonction du type d'Internet de l'utilisateur ou de l'état de charge. Par exemple, si l'utilisateur est en WiFi pendant la charge, vous pouvez prérécupérer beaucoup plus de données que si l'utilisateur est sur Internet mobile avec une batterie faible. Tenir compte de toutes ces variables peut être une chose difficile, ce que peu de gens feraient. Heureusement, il y a GCM Network Manager à la rescousse !
GCM Network Manager est une classe très utile avec de nombreux attributs personnalisables. Vous pouvez facilement planifier des tâches répétitives et ponctuelles. Lors des tâches répétitives, vous pouvez définir l'intervalle de répétition le plus bas ainsi que le plus élevé. Cela permettra de regrouper non seulement vos demandes, mais également les demandes provenant d'autres applications. La radio ne doit être réveillée qu'une fois par période, et pendant qu'elle est allumée, toutes les applications de la file d'attente téléchargent et téléchargent ce qu'elles sont censées faire. Ce gestionnaire est également conscient du type de réseau et de l'état de charge de l'appareil, vous pouvez donc vous adapter en conséquence. Vous pouvez trouver plus de détails et d'échantillons dans cet article, je vous invite à le vérifier. Un exemple de tâche ressemble à ceci :
Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();
D'ailleurs, depuis Android 3.0, si vous faites une requête réseau sur le thread principal, vous obtiendrez une NetworkOnMainThreadException
. Cela vous avertira certainement de ne plus recommencer.
5. Réflexion
La réflexion est la capacité des classes et des objets à examiner leurs propres constructeurs, champs, méthodes, etc. Il est généralement utilisé pour la compatibilité descendante, pour vérifier si une méthode donnée est disponible pour une version particulière du système d'exploitation. Si vous devez utiliser la réflexion à cette fin, assurez-vous de mettre en cache la réponse, car l'utilisation de la réflexion est assez lente. Certaines bibliothèques largement utilisées utilisent également Reflection, comme Roboguice pour l'injection de dépendances. C'est la raison pour laquelle vous devriez préférer Dagger 2. Pour plus de détails sur la réflexion, vous pouvez consulter un article séparé.
6. Boîte automatique
L'autoboxing et l'unboxing sont des processus de conversion d'un type primitif en un type Object, ou vice versa. En pratique, cela signifie convertir un int en un entier. Pour ce faire, le compilateur utilise la fonction Integer.valueOf()
en interne. La conversion n'est pas seulement lente, les objets prennent également beaucoup plus de mémoire que leurs équivalents primitifs. Regardons un peu de code.
Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }
Bien que cela prenne 500 ms en moyenne, le réécrire pour éviter l'autoboxing l'accélérera considérablement.
int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }
Cette solution tourne autour de 2 ms, soit 25 fois plus vite. Si vous ne me croyez pas, testez-le. Les chiffres seront évidemment différents par appareil, mais cela devrait quand même être beaucoup plus rapide. Et c'est aussi une étape très simple à optimiser.
D'accord, vous ne créez probablement pas souvent une variable de type Integer comme celle-ci. Mais qu'en est-il des cas où il est plus difficile d'éviter ? Comme dans une carte, où vous devez utiliser des objets, comme Map<Integer, Integer>
? Regardez la solution que beaucoup de gens utilisent.
Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }
L'insertion de 100 000 entiers aléatoires dans la carte prend environ 250 ms à exécuter. Regardez maintenant la solution avec SparseIntArray.
SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }
Cela prend beaucoup moins, environ 50 ms. C'est aussi l'une des méthodes les plus simples pour améliorer les performances, car rien de compliqué n'a à être fait, et le code reste également lisible. Alors que l'exécution d'une application claire avec la première solution prenait 13 Mo de ma mémoire, l'utilisation d'ints primitifs prenait quelque chose de moins de 7 Mo, donc juste la moitié.
SparseIntArray n'est qu'une des collections intéressantes qui peuvent vous aider à éviter l'autoboxing. Une carte comme Map<Integer, Long>
pourrait être remplacée par SparseLongArray
, car la valeur de la carte est de type Long
. Si vous regardez le code source de SparseLongArray
, vous verrez quelque chose d'assez intéressant. Sous le capot, il ne s'agit essentiellement que d'une paire de baies. Vous pouvez également utiliser un SparseBooleanArray
de la même manière.
Si vous lisez le code source, vous avez peut-être remarqué une note indiquant que SparseIntArray
peut être plus lent que HashMap
. J'ai beaucoup expérimenté, mais pour moi, SparseIntArray
a toujours été meilleur en termes de mémoire et de performances. Je suppose que c'est toujours à vous de choisir, d'expérimenter vos cas d'utilisation et de voir lequel vous convient le mieux. Ayez certainement les SparseArrays
dans votre tête lorsque vous utilisez des cartes.
7. SurDraw
Comme je l'ai dit plus haut, lorsque vous optimisez les performances, vous verrez probablement le plus d'avantages dans l'optimisation du code qui s'exécute souvent. L'une des fonctions les plus courantes est onDraw()
. Cela ne vous surprendra peut-être pas qu'il soit responsable du dessin des vues à l'écran. Comme les appareils fonctionnent généralement à 60 ips, la fonction est exécutée 60 fois par seconde. Chaque image a 16 ms pour être entièrement gérée, y compris sa préparation et son dessin, vous devez donc vraiment éviter les fonctions lentes. Seul le fil principal peut dessiner sur l'écran, vous devez donc éviter de faire des opérations coûteuses dessus. Si vous gelez le thread principal pendant plusieurs secondes, vous pourriez obtenir la tristement célèbre boîte de dialogue Application Not Responding (ANR). Pour redimensionner les images, le travail de base de données, etc., utilisez un fil d'arrière-plan.
J'ai vu certaines personnes essayer de raccourcir leur code, pensant que ce serait plus efficace de cette façon. Ce n'est certainement pas la voie à suivre, car un code plus court ne signifie pas du tout un code plus rapide. En aucun cas vous ne devez mesurer la qualité du code par le nombre de lignes.

L'une des choses que vous devriez éviter dans onDraw()
est d'allouer des objets comme Paint. Préparez tout dans le constructeur, afin qu'il soit prêt lors du dessin. Même si vous avez optimisé onDraw()
, vous ne devez l'appeler qu'aussi souvent que nécessaire. Quoi de mieux que d'appeler une fonction optimisée ? Eh bien, n'appelant aucune fonction du tout. Si vous souhaitez dessiner du texte, il existe une fonction d'assistance assez intéressante appelée drawText()
, où vous pouvez spécifier des éléments tels que le texte, les coordonnées et la couleur du texte.
8. ViewHolders
Vous connaissez probablement celui-ci, mais je ne peux pas le sauter. Le modèle de conception Viewholder est un moyen de rendre le défilement des listes plus fluide. C'est une sorte de mise en cache des vues, qui peut sérieusement réduire les appels à findViewById()
et gonfler les vues en les stockant. Cela peut ressembler à quelque chose comme ça.
static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }
Ensuite, dans la fonction getView()
de votre adaptateur, vous pouvez vérifier si vous disposez d'une vue utilisable. Sinon, vous en créez un.
ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");
Vous pouvez trouver beaucoup d'informations utiles sur ce modèle sur Internet. Il peut également être utilisé dans les cas où votre vue de liste contient plusieurs types d'éléments différents, comme certains en-têtes de section.
9. Redimensionner les images
Il y a de fortes chances que votre application contienne des images. Si vous téléchargez des fichiers JPG sur le Web, ils peuvent avoir des résolutions vraiment énormes. Cependant, les appareils sur lesquels ils seront affichés seront beaucoup plus petits. Même si vous prenez une photo avec l'appareil photo de votre appareil, elle doit être réduite avant de s'afficher car la résolution de la photo est beaucoup plus grande que la résolution de l'écran. Redimensionner les images avant de les afficher est une chose cruciale. Si vous essayez de les afficher en pleine résolution, vous manquerez de mémoire assez rapidement. Il y a beaucoup d'écrits sur l'affichage efficace des bitmaps dans les docs Android, je vais essayer de résumer.
Vous avez donc un bitmap, mais vous n'y connaissez rien. Il existe un indicateur utile de Bitmaps appelé inJustDecodeBounds à votre service, qui vous permet de connaître la résolution du bitmap. Supposons que votre bitmap est de 1024x768 et que l'ImageView utilisée pour l'afficher n'est que de 400x300. Vous devez continuer à diviser la résolution du bitmap par 2 jusqu'à ce qu'il soit encore plus grand que l'ImageView donné. Si vous le faites, il sous-échantillonnera le bitmap par un facteur de 2, vous donnant un bitmap de 512x384. Le bitmap sous-échantillonné utilise 4 fois moins de mémoire, ce qui vous aidera beaucoup à éviter la fameuse erreur OutOfMemory.
Maintenant que vous savez comment le faire, vous ne devriez pas le faire. … Du moins, pas si votre application s'appuie fortement sur des images, et ce n'est pas seulement 1-2 images. Évitez certainement des choses comme le redimensionnement et le recyclage manuel des images, utilisez des bibliothèques tierces pour cela. Les plus populaires sont Picasso de Square, Universal Image Loader, Fresco de Facebook ou mon préféré, Glide. Il existe une énorme communauté active de développeurs autour de lui, vous pouvez donc également trouver de nombreuses personnes utiles dans la section des problèmes sur GitHub.
10. Mode strict
Le mode strict est un outil de développement très utile que beaucoup de gens ne connaissent pas. Il est généralement utilisé pour détecter les requêtes réseau ou les accès disque à partir du thread principal. Vous pouvez définir les problèmes que le mode strict doit rechercher et la pénalité qu'il doit déclencher. Un exemple Google ressemble à ceci :
public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }
Si vous souhaitez détecter tous les problèmes que le mode strict peut trouver, vous pouvez également utiliser detectAll()
. Comme pour de nombreux conseils de performance, vous ne devriez pas essayer aveuglément de corriger tous les rapports du mode strict. Il suffit d'enquêter dessus, et si vous êtes sûr que ce n'est pas un problème, laissez-le tranquille. Assurez-vous également d'utiliser le mode strict uniquement pour le débogage et de toujours le désactiver sur les versions de production.
Performances de débogage : la méthode Pro
Voyons maintenant quelques outils qui peuvent vous aider à trouver des goulots d'étranglement, ou au moins montrer que quelque chose ne va pas.
1. Moniteur Android
Il s'agit d'un outil intégré à Android Studio. Par défaut, vous pouvez trouver le moniteur Android dans le coin inférieur gauche et vous pouvez basculer entre 2 onglets. Logcat et moniteurs. La section Moniteurs contient 4 graphiques différents. Réseau, CPU, GPU et Mémoire. Ils sont assez explicites, je vais donc les parcourir rapidement. Voici une capture d'écran des graphiques pris lors de l'analyse de JSON lors de son téléchargement.
La partie Réseau affiche le trafic entrant et sortant en Ko/s. La partie CPU affiche l'utilisation du CPU en pourcentage. Le moniteur GPU affiche le temps nécessaire pour rendre les cadres d'une fenêtre d'interface utilisateur. C'est le moniteur le plus détaillé de ces 4, donc si vous voulez plus de détails à ce sujet, lisez ceci.
Enfin, nous avons le moniteur de mémoire, que vous utiliserez probablement le plus. Par défaut, il affiche la quantité actuelle de mémoire libre et allouée. Vous pouvez également forcer un Garbage Collection avec lui, pour tester si la quantité de mémoire utilisée diminue. Il a une fonctionnalité utile appelée Dump Java Heap, qui créera un fichier HPROF qui peut être ouvert avec le visualiseur et l'analyseur HPROF. Cela vous permettra de voir combien d'objets vous avez alloués, quelle quantité de mémoire est utilisée par quoi, et peut-être quels objets causent des fuites de mémoire. Apprendre à utiliser cet analyseur n'est pas la tâche la plus simple, mais cela en vaut la peine. La prochaine chose que vous pouvez faire avec le moniteur de mémoire est de faire un suivi d'allocation chronométré, que vous pouvez démarrer et arrêter comme vous le souhaitez. Cela pourrait être utile dans de nombreux cas, par exemple lors du défilement ou de la rotation de l'appareil.
2. Surcharge GPU
Il s'agit d'un outil d'assistance simple, que vous pouvez activer dans les options de développement une fois que vous avez activé le mode développeur. Sélectionnez Debug GPU overdraw, "Show overdraw areas", et votre écran aura des couleurs bizarres. C'est bon, c'est ce qui doit arriver. Les couleurs signifient combien de fois une zone particulière a été à découvert. La vraie couleur signifie qu'il n'y a pas eu de découvert, c'est ce que vous devriez viser. Le bleu signifie un overdraw, le vert signifie deux, le rose trois, le rouge quatre.
Bien que voir la vraie couleur soit le meilleur, vous verrez toujours des surimpressions, en particulier autour des textes, des tiroirs de navigation, des boîtes de dialogue et plus encore. N'essayez donc pas de vous en débarrasser complètement. Si votre application est bleuâtre ou verdâtre, c'est probablement bien. Cependant, si vous voyez trop de rouge sur certains écrans simples, vous devriez enquêter sur ce qui se passe. Il se peut qu'il y ait trop de fragments empilés les uns sur les autres, si vous continuez à les ajouter au lieu de les remplacer. Comme je l'ai mentionné ci-dessus, le dessin est la partie la plus lente des applications, il est donc inutile de dessiner quelque chose s'il y a plus de 3 calques dessinés dessus. N'hésitez pas à consulter vos applications préférées avec. Vous verrez que même les applications avec plus d'un milliard de téléchargements ont des zones rouges, alors allez-y doucement lorsque vous essayez d'optimiser.
3. Rendu GPU
Il s'agit d'un autre outil des options du développeur, appelé rendu GPU de profil. Après la sélection, choisissez "Sur l'écran sous forme de barres". Vous remarquerez que des barres colorées apparaissent sur votre écran. Étant donné que chaque application a des barres distinctes, bizarrement, la barre d'état a les siennes, et si vous avez des boutons de navigation logicielle, ils ont aussi leurs propres barres. Quoi qu'il en soit, les barres sont mises à jour lorsque vous interagissez avec l'écran.
Les barres sont composées de 3 à 4 couleurs et, selon les documents Android, leur taille compte en effet. Plus c'est petit, mieux c'est. En bas, vous avez le bleu, qui représente le temps utilisé pour créer et mettre à jour les listes d'affichage de la vue. Si cette partie est trop haute, cela signifie qu'il y a beaucoup de dessin de vue personnalisé, ou beaucoup de travail effectué dans les fonctions onDraw()
. Si vous avez Android 4.0+, vous verrez une barre violette au-dessus de la bleue. Cela représente le temps passé à transférer des ressources vers le thread de rendu. Vient ensuite la partie rouge, qui représente le temps passé par le moteur de rendu 2D d'Android à envoyer des commandes à OpenGL pour dessiner et redessiner des listes d'affichage. En haut se trouve la barre orange, qui représente le temps pendant lequel le CPU attend que le GPU termine son travail. S'il est trop grand, l'application fait trop de travail sur le GPU.
Si vous êtes assez bon, il y a une couleur de plus au-dessus de l'orange. C'est une ligne verte représentant le seuil de 16 ms. Comme votre objectif doit être d'exécuter votre application à 60 ips, vous disposez de 16 ms pour dessiner chaque image. Si vous ne le faites pas, certaines images pourraient être ignorées, l'application pourrait devenir saccadée et l'utilisateur le remarquerait certainement. Portez une attention particulière aux animations et au défilement, c'est là que la fluidité compte le plus. Même si vous pouvez détecter certaines images sautées avec cet outil, cela ne vous aidera pas vraiment à déterminer où se situe exactement le problème.
4. Visionneuse de hiérarchie
C'est l'un de mes outils préférés, car il est vraiment puissant. Vous pouvez le démarrer depuis Android Studio via Tools -> Android -> Android Device Monitor, ou il se trouve également dans votre dossier sdk/tools en tant que "monitor". Vous pouvez également y trouver un exécutable hierarachyviewer autonome, mais comme il est obsolète, vous devez ouvrir le moniteur. Quelle que soit la façon dont vous ouvrez Android Device Monitor, passez à la perspective Hierarchy Viewer. Si vous ne voyez aucune application en cours d'exécution attribuée à votre appareil, vous pouvez faire plusieurs choses pour y remédier. Essayez également de consulter ce fil de discussion, il y a des gens avec toutes sortes de problèmes et toutes sortes de solutions. Quelque chose devrait fonctionner pour vous aussi.
Avec Hierarchy Viewer, vous pouvez obtenir un aperçu très net de vos hiérarchies de vues (évidemment). Si vous voyez chaque mise en page dans un XML séparé, vous pouvez facilement repérer des vues inutiles. Cependant, si vous continuez à combiner les mises en page, cela peut facilement devenir déroutant. Un outil comme celui-ci permet de repérer facilement, par exemple, un RelativeLayout, qui n'a qu'un seul enfant, un autre RelativeLayout. Cela rend l'un d'eux amovible.
Évitez d'appeler requestLayout()
, car cela entraîne la traversée de toute la hiérarchie des vues, pour déterminer la taille de chaque vue. S'il y a un conflit avec les mesures, la hiérarchie peut être parcourue plusieurs fois, ce qui, si cela se produit pendant une animation, fera certainement sauter certaines images. Si vous voulez en savoir plus sur la façon dont Android dessine ses vues, vous pouvez lire ceci. Regardons une vue comme on le voit dans Hierarchy Viewer.
Le coin supérieur droit contient un bouton pour maximiser l'aperçu de la vue particulière dans une fenêtre autonome. En dessous, vous pouvez également voir l'aperçu réel de la vue dans l'application. L'élément suivant est un nombre, qui représente le nombre d'enfants de la vue donnée, y compris la vue elle-même. Si vous sélectionnez un nœud (de préférence le nœud racine) et appuyez sur "Obtenir les temps de mise en page" (3 cercles colorés), vous aurez 3 valeurs supplémentaires remplies, ainsi que des cercles colorés apparaissant étiquetés mesurer, mettre en page et dessiner. Il n'est peut-être pas surprenant que la phase de mesure représente le temps qu'il a fallu pour mesurer la vue donnée. La phase de mise en page concerne le temps de rendu, tandis que le dessin est l'opération de dessin proprement dite. Ces valeurs et couleurs sont relatives les unes aux autres. Le vert signifie que la vue s'affiche dans les 50 % supérieurs de toutes les vues de l'arborescence. Le jaune signifie un rendu dans les 50 % les plus lents de toutes les vues de l'arborescence, le rouge signifie que la vue donnée est l'une des plus lentes. Comme ces valeurs sont relatives, il y aura toujours des rouges. Vous ne pouvez tout simplement pas les éviter.
Sous les valeurs, vous avez le nom de la classe, tel que "TextView", un ID de vue interne de l'objet, et l'android:id de la vue, que vous avez défini dans les fichiers XML. Je vous invite à prendre l'habitude d'ajouter des identifiants à toutes les vues, même si vous ne les référencez pas dans le code. Cela rendra l'identification des vues dans Hierarchy Viewer très simple, et si vous avez des tests automatisés dans votre projet, cela rendra également le ciblage des éléments beaucoup plus rapide. Cela vous fera gagner du temps, à vous et à vos collègues, en les écrivant. L'ajout d'ID aux éléments ajoutés dans les fichiers XML est assez simple. Mais qu'en est-il des éléments ajoutés dynamiquement ? Eh bien, cela s'avère très simple aussi. Créez simplement un fichier ids.xml dans votre dossier de valeurs et saisissez les champs requis. Cela peut ressembler à ceci :
<resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>
Ensuite, dans le code, vous pouvez utiliser setId(R.id.item_title)
. Rien de plus simple.
Il y a quelques autres choses à faire attention lors de l'optimisation de l'interface utilisateur. Vous devriez généralement éviter les hiérarchies profondes tout en préférant les peu profondes, voire larges. N'utilisez pas de mises en page dont vous n'avez pas besoin. Par exemple, vous pouvez probablement remplacer un groupe de LinearLayouts
imbriqués par un RelativeLayout
ou un TableLayout
. N'hésitez pas à expérimenter différentes mises en page, n'utilisez pas toujours LinearLayout
et RelativeLayout
. Essayez également de créer des vues personnalisées si nécessaire, cela peut améliorer considérablement les performances s'il est bien fait. Par exemple, saviez-vous qu'Instagram n'utilise pas TextViews pour afficher les commentaires ?
Vous pouvez trouver plus d'informations sur Hierarchy Viewer sur le site des développeurs Android avec des descriptions de différents volets, en utilisant l'outil Pixel Perfect, etc. Une autre chose que je voudrais souligner est la capture des vues dans un fichier .psd, ce qui peut être fait par le bouton "Capturer les calques de la fenêtre". Chaque vue sera dans un calque séparé, il est donc très simple de la masquer ou de la modifier dans Photoshop ou GIMP. Oh, c'est une autre raison d'ajouter un ID à chaque vue que vous pouvez. Cela donnera aux calques des noms qui ont du sens.
Vous trouverez beaucoup plus d'outils de débogage dans les options du développeur, je vous conseille donc de les activer et de voir ce qu'ils font. Qu'est ce qui pourrait aller mal?
Le site des développeurs Android contient un ensemble de bonnes pratiques en matière de performances. Ils couvrent beaucoup de domaines différents, y compris la gestion de la mémoire, dont je n'ai pas vraiment parlé. Je l'ai silencieusement ignoré, car la gestion de la mémoire et le suivi des fuites de mémoire sont une toute autre histoire. L'utilisation d'une bibliothèque tierce pour afficher efficacement les images vous aidera beaucoup, mais si vous avez encore des problèmes de mémoire, consultez Leak Canary créé par Square ou lisez ceci.
Emballer
Donc, c'était la bonne nouvelle. La mauvaise nouvelle est que l'optimisation des applications Android est beaucoup plus compliquée. Il existe de nombreuses façons de tout faire, vous devez donc en connaître les avantages et les inconvénients. Il n'y a généralement pas de solution miracle qui n'a que des avantages. Ce n'est qu'en comprenant ce qui se passe dans les coulisses que vous pourrez choisir la solution qui vous convient le mieux. Ce n'est pas parce que votre développeur préféré dit que quelque chose est bon que c'est nécessairement la meilleure solution pour vous. Il y a beaucoup plus de domaines à discuter et plus d'outils de profilage qui sont plus avancés, nous pourrions donc les aborder la prochaine fois.
Assurez-vous d'apprendre des meilleurs développeurs et des meilleures entreprises. Vous pouvez trouver quelques centaines de blogs d'ingénierie sur ce lien. Ce n'est évidemment pas seulement des trucs liés à Android, donc si vous n'êtes intéressé que par Android, vous devez filtrer le blog en particulier. Je recommande vivement les blogs de Facebook et Instagram. Même si l'interface utilisateur d'Instagram sur Android est discutable, leur blog d'ingénierie contient des articles vraiment sympas. Pour moi, c'est génial qu'il soit si facile de voir comment les choses se passent dans des entreprises qui gèrent quotidiennement des centaines de millions d'utilisateurs, donc ne pas lire leurs blogs semble fou. Le monde change très vite, donc si vous n'essayez pas constamment de vous améliorer, d'apprendre des autres et d'utiliser de nouveaux outils, vous serez laissé pour compte. Comme l'a dit Mark Twain, une personne qui ne lit pas n'a aucun avantage sur une personne qui ne sait pas lire.