Пикассо: из памяти
У меня есть 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
, выполняется в основном потоке.
![Threads]()
Ответы
Ответ 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
}