web-dev-qa-db-fra.com

Picasso: hors de mémoire

J'ai un RecyclerView présentant plusieurs images en utilisant Picasso. Après avoir fait défiler un certain temps de haut en bas, l'application manque de mémoire avec des messages comme celui-ci:

E/dalvikvm-heap﹕ Out of memory on a 3053072-byte allocation.
I/dalvikvm﹕ "Picasso-/wp-content/uploads/2013/12/DSC_0972Small.jpg" prio=5 tid=19 RUNNABLE
I/dalvikvm﹕ | group="main" sCount=0 dsCount=0 obj=0x42822a50 self=0x59898998
I/dalvikvm﹕ | sysTid=25347 Nice=10 sched=0/0 cgrp=apps/bg_non_interactive handle=1500612752
I/dalvikvm﹕ | state=R schedstat=( 10373925093 843291977 45448 ) utm=880 stm=157 core=3
I/dalvikvm﹕ at Android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
I/dalvikvm﹕ at Android.graphics.BitmapFactory.decodeStream(BitmapFactory.Java:623)
I/dalvikvm﹕ at com.squareup.picasso.BitmapHunter.decodeStream(BitmapHunter.Java:142)
I/dalvikvm﹕ at com.squareup.picasso.BitmapHunter.hunt(BitmapHunter.Java:217)
I/dalvikvm﹕ at com.squareup.picasso.BitmapHunter.run(BitmapHunter.Java:159)
I/dalvikvm﹕ at Java.util.concurrent.Executors$RunnableAdapter.call(Executors.Java:390)
I/dalvikvm﹕ at Java.util.concurrent.FutureTask.run(FutureTask.Java:234)
I/dalvikvm﹕ at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1080)
I/dalvikvm﹕ at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:573)
I/dalvikvm﹕ at Java.lang.Thread.run(Thread.Java:841)
I/dalvikvm﹕ at com.squareup.picasso.Utils$PicassoThread.run(Utils.Java:411)
I/dalvikvm﹕ [ 08-10 18:48:35.519 25218:25347 D/skia     ]
    --- decoder->decode returned false

Les choses que je note lors du débogage:

  1. Lors de l'installation de l'application sur un téléphone ou un appareil virtuel, les images sont chargées sur le réseau, ce qui est censé être le cas. Ceci est vu par le triangle rouge dans le coin supérieur gauche de l'image.
  2. Lors du défilement pour que les images soient rechargées, elles sont extraites du disque. Ceci est vu par le triangle bleu dans le coin supérieur gauche de l'image.
  3. Lors du défilement, certaines images sont chargées de la mémoire, comme le montre un triangle vert dans le coin supérieur gauche.
  4. Après avoir fait défiler un peu plus, l'exception de mémoire insuffisante se produit et le chargement s'arrête. Seule l'image d'espace réservé est affichée sur les images qui ne sont pas actuellement conservées en mémoire, tandis que celles en mémoire sont affichées correctement avec un triangle vert.

Ici est un exemple d'image. Il est assez grand, mais j'utilise fit() pour réduire l'empreinte mémoire dans l'application.

Mes questions sont donc les suivantes:

  • Les images ne doivent-elles pas être rechargées à partir du disque lorsque le cache mémoire est plein?
  • Les images sont-elles simplement trop grandes? Quelle quantité de mémoire puis-je attendre d'une image, disons 0,5 Mo, à consommer lorsqu'elle est décodée?
  • Y a-t-il quelque chose de mal/inhabituel dans mon code ci-dessous?

Configuration de l'instance Picasso statique lors de la création de Activity:

private void setupPicasso()
{
    Cache diskCache = new Cache(getDir("foo", Context.MODE_PRIVATE), 100000000);
    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.setCache(diskCache);

    Picasso picasso = new Picasso.Builder(this)
            .memoryCache(new LruCache(100000000)) // Maybe something fishy here?
            .downloader(new OkHttpDownloader(okHttpClient))
            .build();

    picasso.setIndicatorsEnabled(true); // For debugging

    Picasso.setSingletonInstance(picasso);
}

Utilisation de l'instance statique de Picasso dans mon RecyclerView.Adapter:

@Override
public void onBindViewHolder(RecipeViewHolder recipeViewHolder, int position)
{
    Picasso.with(mMiasMatActivity)
            .load(mRecipes.getImage(position))
            .placeholder(R.drawable.picasso_placeholder)
            .fit()
            .centerCrop()
            .into(recipeViewHolder.recipeImage); // recipeImage is an ImageView

    // More...
}

Le ImageView dans le fichier XML:

<ImageView
    Android:id="@+id/mm_recipe_item_recipe_image"
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"
    Android:adjustViewBounds="true"
    Android:paddingBottom="2dp"
    Android:layout_alignParentTop="true"
    Android:layout_centerHorizontal="true"
    Android:clickable="true"
/>

Mise à jour

Il semble que le défilement de RecyclerView continue d'augmenter indéfiniment l'allocation de mémoire. J'ai fait un test RecyclerView dépouillé pour correspondre au documentation officielle , en utilisant une seule image pour 200 CardViews avec un ImageView, mais le problème persiste . La plupart des images sont chargées à partir de la mémoire (vert) et le défilement est fluide, mais environ un dixième ImageView charge l'image à partir du disque (bleu). Lorsque l'image est chargée à partir du disque, une allocation de mémoire est effectuée, augmentant ainsi l'allocation sur le tas et donc le tas lui-même.

J'ai essayé de supprimer ma propre configuration de l'instance globale Picasso et d'utiliser la valeur par défaut à la place, mais les problèmes sont les mêmes.

J'ai fait une vérification avec le Android Device Monitor, voir l'image ci-dessous. C'est pour un Galaxy S3. Chacune des allocations effectuées lorsqu'une image est chargée à partir du disque peut être vue à droite sous "Nombre d'allocations par taille". La taille est légèrement différente pour chaque allocation de l'image, ce qui est également étrange. En appuyant sur "Cause GB", l'allocation la plus à droite de 4,7 Mo disparaît.

Android Device Monitor

Le comportement est le même pour les appareils virtuels. L'image ci-dessous le montre pour un Nexus 5 AVD. Ici aussi, la plus grande allocation (celle de 10,6 Mo) disparaît lorsque vous appuyez sur "Cause GB".

Android Device Monitor

De plus, voici des images des emplacements d'allocation de mémoire et des threads du Android Device Monitor. Les allocations récurrentes sont effectuées dans les threads Picasso tandis que celle supprimée avec Cause GB Se fait sur le thread principal.

Allocation TrackerThreads

28
Krøllebølle

Je ne suis pas sûr que fit() fonctionne avec Android:adjustViewBounds="true". Selon certains des numéros précédents cela semble problématique.

Quelques recommandations:

  • Définir une taille fixe pour ImageView
  • Utilisez un GlobalLayoutListener pour obtenir la taille de l'ImageView une fois qu'elle est calculée et après cet appel, Picasso ajoute la méthode resize()
  • Essayez Glide - sa configuration par défaut entraîne une empreinte inférieure à celle de Picasso (il stocke l'image redimensionnée plutôt que l'original et utilise RGB565)
38
Samuil Yanovski
.memoryCache(new LruCache(100000000)) // Maybe something fishy here?

Je dirais que c'est vraiment louche - vous donnez au LruCache 100 Mo d'espace. Bien que tous les appareils soient différents, cela va être au-dessus ou au-dessus de la limite pour certains appareils, et gardez à l'esprit que ce n'est que le LruCache, ne tenant pas compte de l'espace de tas requis par le reste de votre application. Je suppose que c'est la cause directe de l'exception - vous dites au LruCache qu'il est autorisé à devenir beaucoup plus gros qu'il ne devrait l'être.

Je réduirais cela à quelque chose comme 5 Mo pour prouver d'abord la théorie, puis expérimenter avec des valeurs incrémentielles plus élevées sur vos appareils cibles. Vous pouvez également interroger l'appareil sur l'espace dont il dispose et définir cette valeur par programme si vous le souhaitez. Enfin, il y a le Android:largeHeap="true" attribut que vous pouvez ajouter à votre manifeste, mais j'ai compris qu'il s'agit généralement d'une mauvaise pratique.

Vos images sont en effet grandes, je suggère donc de les réduire également. Gardez à l'esprit que même si vous les coupez, ils doivent encore être temporairement chargés dans la mémoire à leur taille maximale.

6
Sam Dozor

avec picasso vous pouvez résoudre le problème en utilisant sa propriété comme:

  Picasso.with(context)
                .load(url)
                .resize(300,300)
                .into(listHolder.imageview);

vous devez redimensionner l'image.

2
Dinesh

Je viens de créer une classe Singleton pour LoadImages. Le problème était que j'utilisais trop d'objets Picasso créés avec trop de Picasso.Builder. Voici ma mise en œuvre:

public class ImagesLoader {

    private static ImagesLoader currentInstance = null;
    private static Picasso currentPicassoInstance = null;

    protected ImagesLoader(Context context) {
        initPicassoInstance(context);
    }

    private void initPicassoInstance(Context context) {
        Picasso.Builder builder = new Picasso.Builder(context);
        builder.listener(new Picasso.Listener() {
            @Override
            public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
                exception.printStackTrace();
            }
        });
        currentPicassoInstance = builder.build();
    }

    public static ImagesLoader getInstance(Context context) {
        if (currentInstance == null) {
            currentInstance = new ImagesLoader(context);
        }
        return currentInstance;
    }

    public void loadImage(ImageToLoad loadingInfo) {
        String imageUrl = loadingInfo.getUrl().trim();
        ImageView destination = loadingInfo.getDestination();
        if (imageUrl.isEmpty()) {
            destination.setImageResource(loadingInfo.getErrorPlaceholderResourceId());
        } else {
            currentPicassoInstance
                    .load(imageUrl)
                    .placeholder(loadingInfo.getPlaceholderResourceId())
                    .error(loadingInfo.getErrorPlaceholderResourceId())
                    .into(destination);
        }
    }
}

Ensuite, vous créez une classe ImageToLoad qui contient ImageView, Url, Placeholder et Error Placeholder.

public class ImageToLoad {

    private String url;
    private ImageView destination;
    private int placeholderResourceId;
    private int errorPlaceholderResourceId;

    //Getters and Setters

}
0
Gabriel Piffaretti