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:
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:
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"
/>
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 CardView
s 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.
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".
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.
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:
resize()
.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.
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.
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
}