Подтвердить что ты не робот

Пикассо: из памяти

У меня есть 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

То, что я отмечаю при отладке:

  • При установке приложения на телефоне или виртуальном устройстве изображения загружаются по сети, как и должно быть. Это видно по красному треугольнику в верхнем левом углу изображения.
  • При прокрутке, чтобы изображения перезагружались, они извлекаются с диска. Это видно по синему треугольнику в верхнем левом углу изображения.
  • При прокрутке некоторых изображений некоторые изображения загружаются из памяти, как видно из зеленого треугольника в верхнем левом углу.
  • После прокрутки еще больше возникает исключение из памяти и загрузка прекращается. На изображениях, которые в настоящее время не хранятся в памяти, отображается только изображение-заглушка, а в памяти отображаются зеленым треугольником.

Здесь - образ образца. Он довольно большой, но я использую fit() для уменьшения объема памяти в приложении.

Итак, мои вопросы:

  • Не следует ли загружать изображения с диска, когда кеш памяти заполнен?
  • Являются ли изображения слишком большими? Сколько памяти я могу ожидать, скажем, 0,5 МБ изображения, потреблять при декодировании?
  • В моем коде ниже есть что-то неправильное/необычное?

Настройка статического экземпляра Picasso при создании 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);
}

Использование статического экземпляра Picasso в 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...
}

ImageView в файле 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"
/>

Update

Кажется, что прокрутка RecyclerView непрерывно увеличивает распределение памяти на неопределенный срок. Я проверил тест RecyclerView, чтобы сопоставить официальную документацию , используя одно изображение для 200 CardView с ImageView, но проблема сохраняется. Большая часть изображений загружается из памяти (зеленая), а прокрутка плавная, но примерно каждая десятая ImageView загружает изображение с диска (синий). Когда изображение загружается с диска, выполняется распределение памяти, тем самым увеличивая выделение в куче и, следовательно, самой куче.

Я попытался удалить свою собственную настройку глобального экземпляра Picasso и использовать по умолчанию вместо этого, но проблемы одинаковы.

Я проверил монитор Android-устройств, см. изображение ниже. Это для Галактики S3. Каждое из распределений, выполняемых при загрузке изображения с диска, можно увидеть справа в разделе "Счет подсчета для каждого размера". Размер немного отличается для каждого размещения изображения, что тоже странно. Нажатие кнопки "Причина GB" делает самое правильное выделение 4,7 МБ.

Android Device Monitor

Поведение одинаково для виртуальных устройств. На рисунке ниже показано его для AVS Nexus 5. Также здесь наибольшее распределение (10,6 МБ) уходит при нажатии "Причина GB".

Android Device Monitor

Кроме того, здесь представлены изображения местоположений выделения памяти и потоки от Android Device Monitor. Повторяющиеся выделения выполняются в потоках Picasso, а один, удаленный с помощью Cause GB, выполняется в основном потоке.

Tracker Allocation Threads

4b9b3361

Ответ 1

Я не уверен, что fit() работает с android:adjustViewBounds="true". Согласно некоторым из прошлых выпусков, это кажется проблематичным.

Несколько рекомендаций:

  • Установить фиксированный размер для ImageView
  • Пользователь a GlobalLayoutListener, чтобы получить размер ImageView после его вычисления, и после этого вызова Picasso добавляет метод resize()
  • Дайте Glide попытку - его конфигурация по умолчанию приводит к более низкому значению, чем Picasso (в нем хранятся измененные изображения, а не оригиналы и использует RGB565)

Ответ 2

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

Я бы сказал, что это действительно подозрительно - вы даете LruCache 100 МБ пространства. Несмотря на то, что все устройства отличаются друг от друга, на некоторых устройствах это будет или выше предела, и имейте в виду, что это только LruCache, но не учитывает сколько места кучи требует остальная часть вашего приложения. Я предполагаю, что это прямая причина исключения - вы сообщаете LruCache, что это позволило получить намного больше, чем должно быть.

Я бы уменьшил это до 5 МБ, чтобы сначала доказать теорию, а затем экспериментировать с более высокими значениями на ваших целевых устройствах. Вы также можете запросить устройство, сколько места у него есть, и установить это значение программно, если хотите. Наконец, есть атрибут android:largeHeap="true", который вы можете добавить в свой манифест, но я собрал это, как правило, плохую практику.

Ваши изображения действительно большие, поэтому я бы предложил уменьшить их. Имейте в виду, что, хотя вы их обрезаете, они по-прежнему должны быть загружены в память в полном объеме.

Ответ 3

с picasso вы можете решить проблему, используя ее свойство     как:

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

вам нужно изменить размер изображения.

Ответ 4

Я просто создал класс Singleton для LoadImages. Проблема заключалась в том, что я использовал слишком много объектов Picasso, созданных со слишком большим количеством Picasso.Builder. Здесь моя реализация:

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

Затем вы создаете класс ImageToLoad, содержащий объекты ImageView, Url, Placeholder и Error Placeholder.

public class ImageToLoad {

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

    //Getters and Setters

}