TextView.setText() à haute fréquence : fuite mémoire

Cet article présente pourquoi Android TextView est responsable d’une fuite mémoire lorsqu’il s’agit de mettre à jour du texte à haute fréquence, et comment contourner le problème. Avertissement : je partage dans cet article mon expérience de débutant en Java sur Android qui aurai apprécié lire un tel article avant. J’ai pu manquer quelque chose.

Le problème : l’importante création d’objets

J’affiche toutes les 50ms de nouvelles valeurs issues de capteurs du smartphone : accéléromètre, gyroscope, et d’autres capteurs qu’ils soient matériels ou issus de la fusion d’autres capteurs. Voilà ce qui se passe au niveau de la mémoire :

Aperçu de l'utilisation de la mémoire RAM qui grimpe en flèche
Toutes les 15 secondes, le garbage collector vide la mémoire

Sans parler des pauses de quelques millisecondes qu’impose l’opération de libération mémoire, le garbage collector n’est pas censé être autant sollicité. Après avoir réduit à néant toute construction d’objet (notamment d’objet String) dans ma boucle répétée, je me suis intéressé donc à TextView, et j’ai lancé une analyse de mémoire :

Aperçu du résultat de l'analyse mémoire
L’arbre d’appel affiche les opérations dues à setText()

Chaque appel à setText() implique la création d’un nouveau StaticLayout, puis recalcule exactement toutes les proportions du layout en fonction du contenu. Sauf que chaque création de StaticLayout implique elle-même la création de nombreux objets en mémoire.

Dans mon cas, setText() est très sollicité puisque j’ai une dizaine de layouts à mettre à jour constamment. Malheureusement réduire tout mon contenu à un seul layout ne change rien non plus, à haute fréquence même la création d’un seul StaticLayout est très consommatrice en mémoire. Il faut éviter au maximum la création d’objets.

Une solution : ne pas utiliser de TextView

Afin d’éviter ça, j’ai donc créé un nouveau type de View qui agit comme un layout spécialisé, de dimensions précises (car mon contenu prend toujours une taille fixe en hauteur, et quasi la même taille fixe en largeur) auquel je modifie son évènement onDraw() (l’événement appelé pour directement écrire moi-même le texte en utilisant Canvas.drawText()), le but étant de court-circuiter le comportement de base appliqué au TextView.

public class SensorView extends View {

    ...

    private final char[] buffer = new char[BUFFER_SIZE];
    private final Paint paint = new Paint();

    public SensorView(Context context) {
        super(context);
        this.initPaint();
    }

    public SensorView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // Get back some values from xml data
        this.padding = attrs.getAttributeIntValue("http://schemas.android.com/apk/res/android", "padding", this.padding);
        this.textSize = attrs.getAttributeIntValue("http://schemas.android.com/apk/res/android", "padding", this.textSize);
        this.initPaint();
    }

    public void initPaint() {

        // Setting the paint
        this.padding = getPixels(TypedValue.COMPLEX_UNIT_DIP, this.padding);
        this.textSize = getPixels(TypedValue.COMPLEX_UNIT_SP, this.textSize);

        this.paint.setAntiAlias(true);
        this.paint.setColor(Color.DKGRAY);
        this.paint.setTextSize(this.textSize);
    }

    @Override
    public void onDraw(Canvas canvas) {

        int start = 0, line = 0;

        // Display multi-lines content
        for (int i = 0; i < BUFFER_SIZE; i++)
        {
            if (buffer[i] == '\n' || buffer[i] == '\0') {
                canvas.drawText(buffer, start, i-start, this.padding, line++ * this.textSize * LINE_HEIGHT + this.padding, paint);
                start = i+1;
            }
        }
    }

    public void setText(StringBuilder source) {
        source.getChars(0, source.length(), buffer, 0);
    }

    private int getPixels(int unit, float size) {
        DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
        return (int)TypedValue.applyDimension(unit, size, metrics);
    }
}

A chaque modification, j’appelle SensorView.postInvalidate() qui invalide la View et refait appel à onDraw(). Bien sûr, je ne profite plus des optimisations de performance liées à TextView, mais de toute manière je suis certain que mon texte n’est jamais le même et ne pourra pas profiter de quelconque mise en cache. Voici le nouveau résultat côté mémoire :

Aperçu de l'utilisation de la mémoire RAM après correction
Après correction, la mémoire ne grimpe plus en flèche

Problème réglé !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *