• 20 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/12/19

Trouvez facilement les fuites de mémoire

Au début de cette partie, quand nous avons créé notre première AsyncTask, je vous ai parlé d'un problème assez récurrent lorsque l'on travaille avec les Threads sur Android : Les fuites de mémoire ou Memory Leaks.

Afin de  comprendre ce phénomène, il faut que l'on aborde en premier lieu la manière dont la mémoire est gérée dans une application.

Quand vous développez une application Android en Java, vous n'avez pas besoin de gérer vous-même la mémoire, et heureusement ! Lorsque vous créez un objet ( new MonObjet() ), une plage mémoire lui est automatiquement attribuée. Une fois que vous avez définitivement terminé d'utiliser cet objet, Android supprime automatiquement l'objet, grâce au Garbage Collector ou Ramasse-Miette en Français.

Le rôle du Garbage Collector est assez simple : Trouver des objets dans notre application qui ne seront plus utilisés et récupérer les ressources (mémoires) qu'ils utilisent. La fréquence de passage de ce dernier dépends du système Android en fonction de divers critères, vous n'avez donc pas à le gérer dans votre code.

Bon bah c'est super si le Garbage Collector s'occupe de tout, où est le problème ?

Le problème c'est que le Garbage Collector (GC) est très gentil et très prudent : il ne va supprimer l'objet en mémoire que si ce dernier n'est plus utilisé. Donc si le GC pense que l'objet en question est toujours utilisé, il ne va PAS le supprimer... :-°

Et c'est là le piège pour vous, développeurs : parfois, sans forcément s'en rendre compte, nous créons des Memory Leaks, notamment quand nous travaillons avec les Threads.

Si un objet est Leaked (toujours utilisé alors qu'il ne devrait plus l'être), le GC ne pourra pas le collecter et ce dernier restera en mémoire. Petit à petit alors, de manière assez fourbe, des objets sont leaked et restent en mémoire, ne pouvant donc JAMAIS être supprimés par le GC : la mémoire allouée à votre application étant limitée, celle-ci arrive inexorablement à un trop plein, déborde et... Android n'a plus d'autre choix que de fermer brutalement votre application en levant l'erreur "OutOfMemoryError".

Très souvent, les développeurs Android leaks l'activité (ou le fragment) en elle/lui-même, ce qui surcharge très rapidement la mémoire quand on commence à avoir beaucoup de vues sur chaque écran.

Diagnostiquer un Memory Leak

Pour cela, nous allons créer une simple AsyncTask qui va créer une fuite de mémoire. Reprenez votre mini-application Freezap, disponible également à ce commit Github.

Nous allons créer une classe MyAsyncTaskLeaked (AsyncTask exécutant notre longue tâche en arrière-plan) et qui va créer malgré elle, une fuite de mémoire dans notre application. Puis nous verrons comment la diagnostiquer.

Classe MyAsyncTaskLeaked.java :

public class MyAsyncTaskLeaked extends AsyncTask<Void, Void, Long> {

    private final ProgressBar progressBar;

    // Constructor
    public MyAsyncTaskLeaked(ProgressBar progressBar){
        this.progressBar = progressBar;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        this.progressBar.setVisibility(View.VISIBLE);
        Log.e("TAG", "AsyncTask is started.");
    }

    @Override
    protected void onPostExecute(Long success) {
        super.onPostExecute(success);
        this.progressBar.setVisibility(View.GONE);
        Log.e("TAG", "AsyncTask is finished.");
    }

    @Override
    protected Long doInBackground(Void... voids) {
        Log.e("TAG", "AsyncTask doing some big work...");
        return Utils.executeLongActionDuring7seconds(); // Execute our task
    }
}

Explications : Rien de compliqué ici, c'est une simple AsyncTask que nous créons là. Nous exécutons notre longue tâche en arrière-plan (dans un Thread à part) et nous mettons à jour notre ProgressBar au début et à la fin de la tâche.

Appelons maintenant cette tâche au démarrage de notre activité principale.

Extrait de MainActivity.java :

public class MainActivity extends AppCompatActivity implements MyAsyncTask.Listeners, LoaderManager.LoaderCallbacks<Long> {

    ...
    
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ....

        //Start AsyncTask leaked
        this.startAsyncTaskLeaked();
    }
    
    ...
    
    private void startAsyncTaskLeaked(){
        new MyAsyncTaskLeaked(this.progressBar).execute();
    }
    
    ...
}

Explications : Nous avons créé ici une méthode (  startAsyncTaskLeaded()  ) nous permettant d'exécuter notre tâche MyAsyncTaskLeaked. On place bien sûr cette méthode dans onCreate() afin de l'appeler dès que l'activité démarrera.

Lancez maintenant votre application. Votre AsyncTask à l'air de bien fonctionner non ?

                                                                     

Mais les fuites de mémoire sont fourbes, car on ne peut pas vraiment les voir comme ça, à l'oeil nu. Surtout si vous êtes un pirate borgne... :pirate:

Afin de remédier à ce problème, nous allons utiliser Android Studio et ses outils d'analyse. Pour commencer, cliquez sur l'onglet "Android Profiler" d'Android Studio en bas à gauche de votre écran.

Onglet
Onglet "Android Profile"

Une seconde vue s'affiche avec l'utilisation de la mémoire (MEMORY), l'utilisation du CPU (CPU) et l'utilisation réseau (NETWORK) de votre application en cours d'exécution.

Double-cliquez sur le graphique de mémoire afin d'avoir accès à un peu plus d'informations concernant l'analyse de notre mémoire, ainsi que des outils supplémentaires.

Memory Profiler
Memory Profiler

Outil 1 [ FORCE GARBAGE COLLECTION ]: Si vous appuyez sur cette icône, vous forcerez l'exécution du Garbage Collector. Cela peut être pratique pour vérifier si vos objets sont correctement détruits quand ils ne servent plus. 

Outil 2 [ DUMP JAVA HEAP ]: Celui-ci permet de capturer une image complète de la mémoire partagée (Heap) de l'ensemble de votre application. C'est grâce à cet outil que nous allons pouvoir vérifier que nous n'avons pas de fuites de mémoire dans celle-ci.

Maintenant, relancez votre application et effectuez diverses rotations pour forcer l'activité à se détruire, puis cliquez assez rapidement sur le bouton correspondant à l'outil 2.

Une fois terminée, un écran devrait apparaitre affichant l'image mémoire dans un grand tableau. Exportez-le à la racine de votre projet, dans un fichier que vous appellerez traces.hprof, en cliquant sur l'icône d'exportation.

Aperçu de l'image mémoire (Heap)
Aperçu de l'image mémoire (Heap)

Puis, ouvrez ce fichier que vous devriez apercevoir dans l'explorateur Android Studio. Une fois ouvert, cliquez sur "Analyzer Task" qui nous permettra d'analyser plus facilement notre trace.

Ouverture du fichier traces.hprof
Ouverture du fichier traces.hprof

Lancez ensuite l'analyseur de tâches en cliquant simplement sur l'icône verte.

                                                             

L'analyseur tentera alors de trouver des fuites de mémoire dans votre code, afin que vous puissiez les résoudre par la suite. Ici, il nous trouve trois activités qui sont leaked, et qui ne peuvent donc pas être supprimées par le Garbage Collector...

Leak de trois activités
Leak de trois activités

On remarque également que c'est notre ProgressBar qui semble être à l'origine du Leak... Et cela est normal ! En effet, nous avons passé celle-ci à notre AsyncTask lors de sa création (dans son constructeur), pour ensuite la stocker à l'intérieur dans une variable de classe, via une référence forte, utilisée par défaut en JAVA.

C'est pour cette raison que nous avons créé, dans le premier chapitre, une référence faible (grâce à une WeakReference) de cette ProgressBar, afin de ne pas créer un "lien trop fort" entre la ProgressBar (et par extension l'activité qui la contient) et l'AsyncTask.

Une référence faible permettra de dire explicitement au Garbage Collector que l'objet référencé peut être supprimé SI et seulement SI ce dernier est référencé uniquement par une ou plusieurs WeakReference.

Essayez maintenant de modifier MyAsyncTaskLeaked afin de retirer la fuite de mémoire grâce à une WeakReference, sans regarder tout de suite la solution bien sûr... :)

Solution (MyAsyncTaskLeaked.java) :

public class MyAsyncTaskLeaked extends AsyncTask<Void, Void, Long> {

    private final WeakReference<ProgressBar> progressBar;

    // Constructor
    public MyAsyncTaskLeaked(ProgressBar progressBar){
        this.progressBar = new WeakReference<>(progressBar);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        if (this.progressBar.get() != null) this.progressBar.get().setVisibility(View.VISIBLE);
        Log.e("TAG", "AsyncTask is started.");
    }

    @Override
    protected void onPostExecute(Long success) {
        super.onPostExecute(success);
        if (this.progressBar.get() != null) this.progressBar.get().setVisibility(View.GONE);
        Log.e("TAG", "AsyncTask is finished.");
    }

    @Override
    protected Long doInBackground(Void... voids) {
        Log.e("TAG", "AsyncTask doing some big work...");
        return Utils.executeLongActionDuring7seconds(); // Execute our task
    }
}

Relancez l'analyse de mémoire plus haut après plusieurs rotations, vous verrez que la fuite de mémoire a disparu... :soleil:

Vous savez maintenant chercher manuellement une fuite de mémoire grâce à Android Studio. Afin de compléter cette analyse, n'hésitez pas à installer l'application Canary Leak, qui permet d'automatiser la recherche de Memory Leaks sur votre application.

En complément de l'analyse manuelle, aucun Memory Leak ne pourra vous surprendre, c'est une certitude ! :D

Conclusion

Et voilà ! Vous savez maintenant correctement créer des tâches en arrière-plan sur Android grâce aux AsyncTask et AsyncTaskLoader. Vous avez également vu comment en planifier grâce à AlarmManager et JobScheduler.

Enfin, vous venez de comprendre l'importance de faire attention aux fameuses fuites de mémoire et comment les repérer grâce à Android Studio.

Example of certificate of achievement
Example of certificate of achievement