2015-08-10 16 views
25

Ho un RecyclerView che presenta diverse immagini utilizzando Picasso. Dopo lo scorrimento po 'di tempo su e giù per l'applicazione esaurisce la memoria con messaggi come questo:Picasso: memoria esaurita

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 

Le cose che nota durante il debug:

  1. Quando si installa l'applicazione su un telefono o di un dispositivo virtuale, le immagini vengono caricate sulla rete, che è come dovrebbe essere. Questo è visto dal triangolo rosso nell'angolo in alto a sinistra dell'immagine.
  2. Durante lo scorrimento in modo che le immagini vengano ricaricate, vengono recuperate dal disco. Questo è visto dal triangolo blu nell'angolo in alto a sinistra dell'immagine.
  3. Durante lo scorrimento, alcune delle immagini vengono caricate dalla memoria, come mostrato da un triangolo verde nell'angolo in alto a sinistra.
  4. Dopo averlo fatto scorrere ancora, si verifica l'eccezione di memoria esaurita e il caricamento si interrompe. Solo l'immagine segnaposto viene mostrata sulle immagini che non sono attualmente conservate in memoria, mentre quelle in memoria sono mostrate correttamente con un triangolo verde.

Here è un'immagine di esempio. È abbastanza grande, ma io uso fit() per ridurre il footprint di memoria nell'app.

Quindi le mie domande sono:

  • Non dovrebbe essere ricaricati le immagini dal disco quando la cache di memoria è piena?
  • Le immagini sono troppo grandi? Quanta memoria posso aspettare un'immagine, diciamo 0.5 MB, da consumare quando decodificato?
  • C'è qualcosa di sbagliato/insolito nel mio codice qui sotto?

Impostare l'istanza di Picasso statica durante la creazione del 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); 
} 

utilizzando l'istanza Picasso statica nel mio 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... 
} 

Il ImageView in 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" 
/> 

Aggiornamento

Sembra che scorrendo il RecyclerView rende continuamente l'aumento di allocazione della memoria a tempo indeterminato. Ho eseguito il test RecyclerView per abbinarlo allo official documentation, utilizzando una singola immagine per 200 CardView s con un ImageView, ma il problema persiste. La maggior parte delle immagini viene caricata dalla memoria (verde) e lo scorrimento è scorrevole, ma circa ogni decimo ImageView carica l'immagine dal disco (blu). Quando l'immagine viene caricata dal disco, viene eseguita un'allocazione di memoria, aumentando così l'allocazione sull'heap e quindi sull'heap stesso.

Ho provato a rimuovere la mia configurazione dell'istanza globale Picasso e invece l'impostazione predefinita, ma i problemi sono gli stessi.

Ho fatto un controllo con il dispositivo Android Monitor, vedere l'immagine qui sotto. Questo è per un Galaxy S3. Ciascuna allocazione eseguita quando un'immagine viene caricata dal disco può essere vista a destra sotto "Conteggio allocazione per dimensione". La dimensione è leggermente diversa per ogni allocazione dell'immagine, che è anche strana. Premendo "Causa GB" si disattiva l'allocazione più a destra di 4,7 MB.

Android Device Monitor

il comportamento è lo stesso per i dispositivi virtuali. L'immagine sotto mostra per un Nexus 5 AVD. Anche qui, l'allocazione più grande (quella da 10,6 MB) scompare quando si preme "Cause GB".

Android Device Monitor

Inoltre, qui immagini delle posizioni di allocazione di memoria e le filettature del monitor dispositivo Android. Le allocazioni ricorrenti vengono eseguite nei thread Picasso mentre quello rimosso con Cause GB viene eseguito sul thread principale.

Allocation Tracker Threads

+0

Sto avendo lo stesso identico problema, anche con solo 25 elementi di riciclaggio, se si scorre su e giù continuamente si blocca con l'eccezione di memoria insufficiente. – Zapnologica

risposta

31

Non sono sicuro che fit() funzioni con android:adjustViewBounds="true". Secondo alcune delle past issues sembra essere problematico.

Alcune raccomandazioni:

  • impostare una dimensione fissa per l'ImageView
  • utente un GlobalLayoutListener per ottenere la dimensione del ImageView una volta che è calcolata e dopo questa chiamata Picasso aggiungendo il metodo resize()
  • Give Glide una prova: la sua configurazione predefinita risulta in un ingombro inferiore rispetto a Picasso (memorizza l'immagine ritagliata anziché l'originale e utilizza RGB565)
+1

Sì, hai ragione. E 'stato il 'android: adjustViewBounds =" true "' che ha incasinato le cose. La rimozione ha fatto sparire immediatamente i problemi. Immagino che anche un'altezza fissa possa essere in ordine in modo che le "ImageViews" siano sicuramente di dimensioni uguali. E forse darò una prova anche a Glide. Saluti! –

6
.memoryCache(new LruCache(100000000)) // Maybe something fishy here? 

direi che questo è davvero sospetto - si sta dando la LruCache 100MB di spazio. Sebbene tutti i dispositivi siano diversi, per alcuni dispositivi questo sarà pari o superiore al limite e tieni presente che si tratta solo dello LruCache, che non tiene conto della quantità di spazio richiesto dal resto dell'app. La mia ipotesi è che questa sia la causa diretta dell'eccezione: stai dicendo allo LruCache che è possibile ottenere molto più grande di quanto dovrebbe essere.

Vorrei ridurlo a qualcosa come 5 MB per provare prima la teoria e quindi sperimentare valori incrementalmente superiori sui dispositivi di destinazione. Puoi anche interrogare il dispositivo per quanto spazio ha e impostare questo valore a livello di codice, se lo desideri. Infine, c'è l'attributo android:largeHeap="true" che puoi aggiungere al tuo manifest, ma ho capito che questo è in genere una cattiva pratica.

Le immagini sono davvero grandi quindi suggerirei di ridurre anche quelle. Tieni presente che anche se li stai tagliando, devono comunque essere caricati temporaneamente nella loro dimensione massima.

+0

Sì, questo ha senso. Ho solo cercato di "massimizzarlo", il che è, ovviamente, una pessima pratica. Quando si utilizza una cache da 10 MB, le immagini vengono sempre caricate dal disco, tranne una, il che rende l'app un po '"spenta", poiché occorrono circa 0,5 secondi per caricare l'immagine. Immagino che tutto si riduce alle immagini troppo grandi. –

+0

È possibile controllare questi thread per sapere come ridurre la quantità di memoria utilizzata dai bitmap una volta decodificati: (1) http://stackoverflow.com/questions/15459834/lrucache-not-working, (2) http: //stackoverflow.com/questions/21392972/how-to-load-large-images-in-android-and-avoiding-the-out-of-memory-error –

+0

Cheers, scaverò in questo e vedrò se posso capisci qualcosa. –

2

con Picasso è possibile risolvere il problema utilizzando la sua proprietà come:

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

è necessario ridimensionare l'immagine.

0

Ho appena creato una classe Singleton per LoadImages. Il problema era che stavo usando troppi oggetti Picasso creati con troppi Picasso.Builder. Ecco la mia realizzazione:

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); 
     } 
    } 
} 

quindi si crea una classe ImageToLoad che contiene l'ImageView, Url, segnaposto e segnaposto errore.

public class ImageToLoad { 

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

    //Getters and Setters 

} 
Problemi correlati