web-dev-qa-db-ja.com

ピカソ:メモリ不足

RecyclerViewを使用して複数の画像を表示するPicassoがあります。上下にスクロールすると、アプリケーションは次のようなメッセージでメモリ不足になります。

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

デバッグ中に注意すること:

  1. 電話または仮想デバイスにアプリをインストールすると、ネットワーク経由で画像が読み込まれます。これは、画像の左上隅にある赤い三角形で示されています。
  2. 画像が再ロードされるようにスクロールすると、ディスクから画像が取得されます。これは、画像の左上隅にある青い三角形で示されています。
  3. さらにスクロールすると、左上隅に緑色の三角形が表示されているように、一部の画像がメモリからロードされます。
  4. さらにスクロールすると、メモリ不足の例外が発生し、ロードが停止します。現在メモリに保存されていない画像にはプレースホルダー画像のみが表示され、メモリ内の画像は緑色の三角形で適切に表示されます。

こちら はサンプル画像です。かなり大きいですが、アプリのメモリフットプリントを減らすためにfit()を使用します。

だから私の質問は:

  • メモリキャッシュがいっぱいになったときに、イメージをディスクからリロードすべきではありませんか?
  • 画像が大きすぎますか? 0.5 MBの画像など、デコード時に消費するメモリ量はどれくらいですか?
  • 以下の私のコードに何か間違っている/異常がありますか?

Activityを作成するときに静的Picassoインスタンスをセットアップする

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

RecyclerView.Adapterで静的Picassoインスタンスを使用する

@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...
}

XMLファイルのImageView

<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"
/>

更新

RecyclerViewを連続的にスクロールすると、メモリ割り当てが無限に増加するようです。 RecyclerViewを持つ200個のCardViewsに対して単一のイメージを使用して、 公式ドキュメント に一致するようにテストImageViewを削除しましたが、問題は解決しません。ほとんどの画像はメモリから読み込まれ(緑)、スクロールはスムーズですが、ImageViewの約10分の1がディスクから画像を読み込みます(青)。イメージがディスクからロードされると、メモリの割り当てが実行され、ヒープ上の割り当て、ひいてはヒープ自体の割り当てが増加します。

グローバルPicassoインスタンスの独自のセットアップを削除して、代わりにデフォルトを使用しようとしましたが、問題は同じです。

Android Device Monitor、下の画像を参照してください。これはGalaxy S3用です。ディスクから画像がロードされるときに行われる各割り当ては、右下に表示されます「サイズごとの割り当て数」:画像の割り当てごとにサイズがわずかに異なりますが、これも奇妙です。「Cause GB」を押すと、4.7 MBの右端の割り当てがなくなります。

Android Device Monitor

動作は仮想デバイスと同じです。下の画像は、Nexus 5 AVDの場合を示しています。また、「Cause GB」を押すと、最大の割り当て(10.6 MBの割り当て)がなくなります。

Android Device Monitor

さらに、メモリ割り当ての場所とAndroid Device Monitorからのスレッド)の画像があります。繰り返し割り当てはPicassoスレッドで行われ、Cause GBで削除された割り当てはメインスレッドで行われます。

Allocation TrackerThreads

28
Krøllebølle

fit()が_Android:adjustViewBounds="true"_で動作するかどうかわかりません。 過去の問題 のいくつかによると、問題があるようです。

いくつかの推奨事項:

  • ImageViewの固定サイズを設定します
  • ユーザー GlobalLayoutListener ImageViewのサイズが計算された後、この呼び出し後にPicassoがresize()メソッドを追加して取得する
  • Glide を試してください-デフォルト設定では、ピカソよりもフットプリントが小さくなります(元のイメージではなく、サイズ変更されたイメージを保存し、RGB565を使用します)
38
Samuil Yanovski
.memoryCache(new LruCache(100000000)) // Maybe something fishy here?

これは確かに怪しいと言えます-あなたはLruCache 100MBのスペースを与えています。すべてのデバイスは異なりますが、これは一部のデバイスの制限以上であり、これはLruCacheのみであり、アプリの他の部分が必要とするヒープスペースを考慮していないことに注意してください。私の推測では、これが例外の直接的な原因であるということです-あなたはLruCacheに、本来よりもずっと大きくなることを許可していると言っているのです。

最初に理論を証明するためにこれを5MBのようなものに減らしてから、ターゲットデバイスで徐々に高い値を試してみます。必要に応じて、デバイスの空き容量をデバイスに照会し、この値をプログラムで設定することもできます。最後に、Android:largeHeap="true"属性をマニフェストに追加できますが、これは一般的に悪い習慣です。

あなたの画像は確かに大きいので、それらも減らすことをお勧めします。トリミングしている場合でも、一時的にフルサイズでメモリにロードする必要があることに注意してください。

6
Sam Dozor

ピカソでは、次のようなプロパティを使用して問題を解決できます。

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

画像のサイズを変更する必要があります。

2
Dinesh

LoadImagesのシングルトンクラスを作成しました。問題は、Picasso.Builderで作成したPicassoオブジェクトが多すぎることです。これが私の実装です。

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

次に、ImageView、Url、Placeholder、Error Placeholderを保持するImageToLoadクラスを作成します。

public class ImageToLoad {

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

    //Getters and Setters

}
0