Cet article décrit une méthode pratique pour diagnostiquer les problèmes de performance à l'aide des outils intégrés à Android Studio, en se concentrant sur l'analyse de l'utilisation du processeur (CPU) et de la mémoire.
Investigation des blocages liés au CPU
Les problèmes courants se manifestent par des interfaces figées ou des messages indiquant que l'application effectue trop de travail sur son thread principal. La cause racine est généralement l'exécution d'opérations longues ou bloquantes sur ce thread.
Démonstration pratique
Considérons un exemple où une activité copie un fichier de manière synchrone lors d'un clic sur un bouton.
public class ChargementDonneesActivite extends AppCompatActivity {
private static final String TAG = "ChargementDonnees";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activite_chargement);
findViewById(R.id.bouton_lancer).setOnClickListener(v -> demarrerChargement());
}
private void demarrerChargement() {
File dossierDestination = new File(Environment.getExternalStorageDirectory(), "BackupData");
dossierDestination.mkdirs();
for (int i = 0; i < 5; i++) {
copierFichierDepuisAssets("donnee_grande.csv", new File(dossierDestination, "copie_" + i + ".dat"));
Log.d(TAG, "Copie numéro " + i + " terminée.");
}
}
}
Ce code génère des avertissements de type « Skipped XX frames! » dans les journaux de l'application.
Utilisation du Profileur CPU
L'outil propose deux techniques d'analyse : l'échantillonnage des piles d'appels (Callstack Sampling) et l'enregistrement des méthodes (Method Recording).
L'échantillonnage offre un aperçu avec une faible surcharge, idéal pour des sessions longues. L'enregistrement des méthodes est précis mais très coûteux en ressources, recommandé uniquement pour des investigations ciblées et courtes.
Pour diagnostiquer l'exemple, on lance un échantillonnage, on reproduit le blocage, puis on arrête l'enregistrement. En filtrant sur le thread principal (souvent nommé d'après le package de l'application), on examine les graphiques. Le graphique en « flamme » (Flame Chart) permet d'identifier rapidement les fonctions dont le temps d'exécution domine. Dans notre cas, la méthode copierFichierDepuisAssets apparaît comme le point chaud principal.
Pour une analyse plus fine de cette fonction, on peut ensuite utiliser l'enregistrement des méthodes, qui confirmera le coût I/O de l'opération et guidera vers la solution : déplacer cette tâche sur un thread en arrière-plan.
Les différentes visualisations
Le profileur fournit quatre vues complémentaires :
- Call Chart : Vue chronologique montrant l'ordre des appels de fonctions.
- Flame Chart : Vue agrégée où la largeur d'une barre représente le pourcentage de temps total consommé par une fonction et ses descendants.
- Top Down : Arbre montrant pour chaque fonction les fonctions qu'elle appelle (enfants) et le temps passé dans son propre code (Self Time).
- Bottom Up : Arbre inversé montrant pour chaque fonction les fonctions qui l'appellent (parents) et le temps total passé dans cette fonction, ventilé par appelant.
Analyse de la mémoire
Problèmes typiques
On distingue principalement deux types de problèmes :
- Fuite de mémoire : Des objets qui ne sont plus nécessaires restent en mémoire car ils sont toujours référencés.
- Épuisement de la mémoire (OOM) : La mémoire disponible est insuffisante pour allouer un nouvel objet, ce qui peut être causé par une fuite ou par un instantané de charge trop importante.
Exemple de fuite de mémoire
Une activité qui crée un thread interne ou un handler avec une référence implicite à elle-même peut présenter une fuite si elle est détruite avant la fin de l'exécution de ces composants.
public class DonneesTemporairesActivite extends AppCompatActivity {
private static final String TAG = "DonneesTempo";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activite_donnees_tempo);
// Création d'une tâche asynchrone qui maintient une référence vers l'activité
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(15000L);
} catch (InterruptedException ignored) {}
Log.d(TAG, "Runnable terminé");
}
}).start();
}
}
Détection d'une fuite
Après avoir navigué plusieurs fois vers et depuis cette activité, on utilise l'option « Analyser l'utilisation de la mémoire » du profileur. La vue affiche les instances restantes de DonneesTemporairesActivite après leur destruction. En sélectionnant une instance, l'onglet « References » liste toutes les chaînes de références qui la maintiennent en vie. L'option cruciale « Afficher uniquement le GC Root le plus proche » simplifie l'analyse en montrant le chemin de référence le plus court depuis un objet racine du ramasse-miettes.
Dans notre exemple, l'objet Runnable anonyme possède un champ implicite this$0 qui référence l'activité. Ce Runnable est lui-même référencé par la file de messages du thread principal, créant ainsi une fuite. La suppression de la référence à l'activité dans le Runnable (par exemple, en utilisant une classe interne statique ou WeakReference) corrige le problème.
Mesures de mémoire clés
- Shallow Size : Taille mémoire directement occupée par l'objet lui-même.
- Retained Size : Taille mémoire totale qui serait libérée si cet objet était supprimé, incluant tous les objets qu'il référence exclusivement.
- Profondeur (Depth) : Distance dans la chaîne de références depuis un GC Root.
Exemple d'épuisement de la mémoire
Un composant graphique (View) qui crée de grandes quantités d'objets Bitmap dans sa méthode onDraw() peut rapidement provoquer un OOM.
public class AnimationComplexeView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Simulation d'une création intensive d'objets
for (int i = 0; i < 100; i++) {
Bitmap buffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvasTemp = new Canvas(buffer);
// ... opérations de dessin sur canvasTemp ...
buffer.recycle(); // Recyclage crucial, mais la surcharge de création reste
}
}
}
Diagnostic de l'épuisement
On utilise l'enregistrement des allocations mémoire (« Suivre la consommation de mémoire »). L'observation montre une croissance rapide du nombre d'objets et une activité fréquente du ramasse-miettes. Après l'arrêt de l'enregistrement, on examine les tables d'objets en filtrant et en triant par classe ou par pile d'appels. On identifie ainsi que la classe Bitmap est allouée en masse depuis la méthode onDraw() de notre vue personnalisée. La solution consiste à pré-allouer les ressources ou à utiliser un mécanisme de mise en cache (Object Pool).