I have a RecyclerView
presenting several images using Picasso
. After scrolling some time up and down the application runs out of memory with messages like this:
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
The things I note while debugging:
Here is a sample image. It is quite large, but I use fit()
to reduce the memory footprint in the app.
So my questions are:
Setting up the static Picasso instance when creating the 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);
}
Using the static Picasso instance in my 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...
}
The ImageView
in the XML file:
<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"
/>
It seems that scrolling the RecyclerView
continuously makes the memory allocation increase indefinitely. I made a test RecyclerView
stripped down to match the official documentation, using a single image for 200 CardView
s with an ImageView
, but the problem persists. Most of the images are loaded from memory (green) and scrolling is smooth, but about every tenth ImageView
loads the image from disk (blue). When the image is loaded from disk, a memory allocation is performed, thereby increasing the allocation on the heap and thus the heap itself.
I tried removing my own setup of the global Picasso
instance and using the default instead, but the problems are the same.
I did a check with the Android Device Monitor, see the image below. This is for a Galaxy S3. Each of the allocations done when an image is loaded from disk can be seen to the right under "Allocation count per size". The size is slightly different for each allocation of the image, which is also weird. Pressing "Cause GB" makes the rightmost allocation of 4.7 MB go away.
The behavior is the same for virtual devices. The image below shows it for a Nexus 5 AVD. Also here, the largest allocation (the 10.6 MB one) goes away when pressing "Cause GB".
Additionally, here are images of the memory allocation locations and the threads from the Android Device Monitor. The reoccurring allocations are done in the Picasso
threads while the one removed with Cause GB
is done on the main thread.
I'm not sure fit()
works with android:adjustViewBounds="true"
. According to some of the past issues it seems to be problematic.
A few recommendations:
resize()
method.memoryCache(new LruCache(100000000)) // Maybe something fishy here?
I would say this is indeed fishy - you're giving the LruCache
100MB of space. Although all devices are different, this is going to be at or above the limit for some devices, and keep in mind that this is only the LruCache
, not accounting for however much heap space the rest of your app requires. My guess is that this is the direct cause of the exception - you're telling the LruCache
that it's allowed to get much bigger than it should be.
I would reduce this to something like 5MB to first prove the theory, and then experiment with incrementally higher values on your target devices. You can also query the device for how much space it has and set this value programmatically if you like. Finally, there's the android:largeHeap="true"
attribute you can add to your manifest, but I've gathered this is generally bad practice.
Your images are indeed large so I would suggest reducing those as well. Keep in mind that even though you're trimming them, they still temporarily need to be loaded into memory at their full size.
with picasso you can solve the problem using its property like:
Picasso.with(context)
.load(url)
.resize(300,300)
.into(listHolder.imageview);
you need to resize the image.
I just made a Singleton class to LoadImages. The problem was that I was using too many Picasso objects created with too many Picasso.Builder. Here's my implementation:
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);
}
}
}
Then you create an ImageToLoad
class that holds the ImageView, Url, Placeholder and Error Placeholder.
public class ImageToLoad {
private String url;
private ImageView destination;
private int placeholderResourceId;
private int errorPlaceholderResourceId;
//Getters and Setters
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With