Как решить проблему OutOfMemoryError при загрузке изображений в Bitmap в Android ListView?
Я разрабатываю Android-приложение с ListView, содержащим кнопки с изображениями в каждой строке. Когда пользователи нажимают на строку списка, запускается новая активность. Я создал пользовательские вкладки из-за проблем с компоновкой камеры. Запущенная активность - это карта, и когда я нажимаю кнопку для запуска предпросмотра изображения (загрузка изображения с SD-карты), приложение возвращается к активности ListView, а затем пытается перезапустить активность предпросмотра изображения.
Предпросмотр изображения в ListView реализован с помощью курсора и ListAdapter. Мне нужно изменять размер изображений (в байтах, а не в пикселях) на лету для атрибута src кнопки с изображением, поэтому я изменяю размер изображений с камеры телефона.
Проблема в том, что я получаю ошибку OutOfMemoryError, когда приложение пытается вернуться и перезапустить вторую активность.
Основные вопросы:
-
Есть ли способ построить адаптер списка строка за строкой, позволяющий изменять размер изображений на лету?
Это было бы предпочтительно, так как мне также нужно изменять свойства виджетов в каждой строке из-за проблем с фокусировкой на сенсорном экране (хотя навигация с помощью шарика работает). -
Я знаю о методе изменения размера и сохранения изображений отдельно, но предпочитаю не использовать этот подход. Однако пример кода для этого метода все равно был бы полезен.
Отключение изображения в ListView решает проблему, что подтверждает, что проблема связана с загрузкой изображений.
Текущая реализация:
String[] from = new String[] { DBHelper.KEY_BUSINESSNAME, DBHelper.KEY_ADDRESS,
DBHelper.KEY_CITY, DBHelper.KEY_GPSLONG, DBHelper.KEY_GPSLAT,
DBHelper.KEY_IMAGEFILENAME + ""};
int[] to = new int[] { R.id.businessname, R.id.address, R.id.city, R.id.gpslong,
R.id.gpslat, R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);
Где R.id.imagefilename - это ButtonImage.
Логи ошибок показывают:
ERROR/dalvikvm-heap: 6291456-byte external allocation too large for this process.
ERROR/AndroidRuntime: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
Дополнительная ошибка при отображении изображений:
DEBUG/skia: xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
INFO/System.out: resolveUri failed on bad bitmap uri:
ERROR/dalvikvm-heap: 6291456-byte external allocation too large for this process.
DEBUG/skia: xxxxxxxxxxxxxxxxxxxx allocPixelRef failed
Как правильно изменять размер изображений, чтобы избежать ошибки OutOfMemoryError, сохраняя качество изображений в моем Android ListView?
Для решения проблемы OutOfMemoryError при загрузке изображений в Bitmap в Android ListView необходимо реализовать правильные техники масштабирования битмапов и стратегии управления памятью. Ключевое решение включает использование BitmapFactory.Options с параметром inSampleSize для изменения размера изображений “на лету” перед загрузкой в память, в сочетании с переработкой битмапов и реализацией механизмов кэширования для предотвращения утечек памяти.
Содержание
- Проблема OutOfMemoryError
- Решения по изменению размера изображений “на лету”
- Управление памятью и переработка
- Альтернативные подходы и библиотеки
- Примеры реализации
- Лучшие практики для загрузки изображений в ListView
Проблема OutOfMemoryError
Ошибка OutOfMemoryError: bitmap size exceeds VM budget возникает, когда виртуальная машина Dalvik Android не может выделить достаточно памяти для загрузки больших битмапов. Это особенно проблематично в сценариях с ListView, где необходимо одновременно загрузить несколько изображений.
Как упоминалось в результатах исследования, ошибка возникает потому, что “предварительный просмотр изображения в ListView выполняется с помощью курсора и ListAdapter”, что может привести к накоплению больших объектов битмапов в памяти. Конкретное сообщение об ошибке 6291456-byte external allocation too large for this process указывает на то, что запрашивается битмап размером 6 МБ, что превышает доступный бюджет памяти.
Основная причина заключается в том, что изображения высокого разрешения с камер телефонов могут потреблять значительный объем памяти. Например, изображение разрешением 12 Мп в полном размере может требовать до 36 МБ памяти (3 байта на пиксель × 12 миллионов пикселей). Когда несколько таких изображений загружаются в строки ListView, память быстро превышает доступный бюджет виртуальной машины.
Решения по изменению размера изображений “на лету”
Использование BitmapFactory.Options с inSampleSize
Наиболее эффективное решение для изменения размера изображений “на лету” включает использование BitmapFactory.Options с параметром inSampleSize. Это позволяет декодировать изображение с меньшим разрешением перед загрузкой в память.
public static Bitmap decodeSampledBitmapFromPath(String path, int reqWidth, int reqHeight) {
// Сначала декодируем с inJustDecodeBounds=true для проверки размеров
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
// Рассчитываем inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Декодируем битмап с установленным inSampleSize
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Исходная высота и ширина изображения
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Рассчитываем наибольшее значение inSampleSize, которое является степенью двойки
// и сохраняет высоту и ширину больше, чем запрошенные.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
Этот подход напрямую отвечает на ваш первый вопрос о построении адаптера списка строка за строкой с изменением размера битмапов “на лету”. Вы можете реализовать это в вашем SimpleCursorAdapter, переопределив метод getView().
Временные битмапы (Purgeable Bitmaps)
Еще один подход - использование опции inPurgeable, которая позволяет системе reclaim память из битмапа при необходимости:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
Однако, согласно результатам исследования, этот метод может быть не всегда надежным, как отмечено в обсуждении Stack Overflow о “Неправильном вызове JPEG-библиотеки в состоянии” ошибках.
Управление памятью и переработка
Правильная переработка битмапов
Как упоминалось в результатах исследования, распространенным решением является “освобождение выделенной памяти при выходе из активности” путем вызова метода recycle() для каждого отображаемого битмапа. Это критически важно для предотвращения утечек памяти:
@Override
protected void onDestroy() {
super.onDestroy();
// Перерабатываем битмапы для освобождения памяти
for (int i = 0; i < notes.getCount(); i++) {
View view = notes.getView(i, null, null);
ImageView imageView = view.findViewById(R.id.imagefilename);
BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
if (drawable != null) {
drawable.getBitmap().recycle();
}
}
}
Реализация обратного вызова onLowMemory
Результаты исследования также рекомендуют реализовать обратный вызов onLowMemory() для проактивного освобождения памяти, когда система испытывает нехватку памяти:
@Override
public void onLowMemory() {
super.onLowMemory();
System.gc(); // очищаем битмапы и другие объекты здесь для уменьшения использования памяти
}
Альтернативные подходы и библиотеки
Использование библиотеки Picasso
Результаты исследования рекомендуют использовать библиотеку Picasso от Square как эффективное решение. Picasso автоматически обрабатывает управление памятью, кэширование и изменение размера:
Picasso.with(context)
.load(url)
.resize(50, 50)
.centerCrop()
.into(imageView);
Этот подход значительно снижает использование памяти за счет изменения размера изображений перед их загрузкой в память. Библиотека также реализует эффективные стратегии кэширования для предотвращения повторной загрузки одних и тех же изображений.
Кэширование на внешнем хранилище
Еще один альтернативный подход, упомянутый в исследованиях, - это “кэшировать загруженные битмапы на внешнем хранилище, таком как SD-карта, и перезагружать их по мере необходимости вместо попытки удерживать все в ОЗУ”. Этот подход включает:
- Изменение размера изображений до соответствующих размеров
- Сохранение их на внешнее хранилище
- Загрузку предварительно измененных изображений при необходимости
Этот метод особенно полезен, если у вас ограниченный объем ОЗУ, но достаточное пространство для хранения.
Примеры реализации
Пользовательский SimpleCursorAdapter с изменением размера “на лету”
Для решения вашей конкретной необходимости в построении адаптера строки за строкой с изменением размера “на лету”, вот полная реализация:
public class CustomCursorAdapter extends SimpleCursorAdapter {
private Context context;
private int layout;
private Cursor cursor;
public CustomCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c, from, to);
this.context = context;
this.layout = layout;
this.cursor = c;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
ImageView imageView = view.findViewById(R.id.imagefilename);
if (cursor.moveToPosition(position)) {
String imagePath = cursor.getString(cursor.getColumnIndex(DBHelper.KEY_IMAGEFILENAME));
if (imagePath != null && !imagePath.isEmpty()) {
// Изменяем размер изображения "на лету"
Bitmap resizedBitmap = decodeSampledBitmapFromPath(
imagePath,
100, // желаемая ширина
100 // желаемая высота
);
imageView.setImageBitmap(resizedBitmap);
}
}
return view;
}
}
Изменение размера и сохранение вне очереди
Для альтернативного подхода, который вы упомянули, вот как реализовать изменение размера и сохранение вне очереди:
public void resizeAndSaveImage(String inputPath, String outputPath, int width, int height) {
Bitmap resizedBitmap = decodeSampledBitmapFromPath(inputPath, width, height);
try {
FileOutputStream out = new FileOutputStream(outputPath);
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 85, out);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
resizedBitmap.recycle();
}
}
Этот подход обрабатывает изображения один раз и сохраняет их в измененном формате, устраняя необходимость в изменении размера “на лету” во время загрузки списка.
Лучшие практики для загрузки изображений в ListView
Реализация паттерна ViewHolder
Для лучшей производительности в ListView реализуйте паттерн ViewHolder, чтобы избежать повторных поисков представлений:
static class ViewHolder {
ImageView imageView;
// другие представления
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(layout, parent, false);
holder = new ViewHolder();
holder.imageView = convertView.findViewById(R.id.imagefilename);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// Загружаем изображение в holder.imageView
// ...
return convertView;
}
LruCache для кэширования в памяти
Реализуйте LruCache для кэширования недавно использованных битмапов в памяти:
private LruCache<String, Bitmap> memoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8; // Используем 1/8 доступной памяти
memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
}
Ленивая загрузка с AsyncTask
Для лучшего пользовательского опыта реализуйте ленивую загрузку с использованием AsyncTask или аналогичных механизмов фоновой загрузки:
class LoadImageTask extends AsyncTask<String, Void, Bitmap> {
private final ImageView imageView;
LoadImageTask(ImageView imageView) {
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(String... params) {
String path = params[0];
return decodeSampledBitmapFromPath(path, 100, 100);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
}
}
Реализуя эти решения, вы можете эффективно решить проблему OutOfMemoryError, сохраняя хорошее качество изображений в вашем приложении Android ListView. Ключ заключается в балансе между качеством изображения и ограничениями памяти путем реализации правильного масштабирования, кэширования и стратегий управления памятью.
Источники
- Android Training - Displaying Bitmaps Efficiently - Официальная документация Android по загрузке битмапов и управлению памятью
- Strange OutOfMemory issue while loading an image to a Bitmap object - Stack Overflow - Комплексное обсуждение проблем с памятью битмапов и решений
- Android Bitmap OutOfMemoryError in ListView - Stack Overflow - Конкретное решение для управления памятью битмапов в ListView
- How to avoid OutOfMemoryError - Stack Overflow - Использование библиотеки Picasso и техники оптимизации памяти
- Out of memory error: vast bitmap - Stack Overflow - Подход к кэшированию на внешнем хранилище
Заключение
Решение проблемы OutOfMemoryError при загрузке изображений в Android ListView требует комплексного подхода, сочетающего правильное изменение размера изображений “на лету”, эффективное управление памятью и стратегическое кэширование. Ключевые выводы:
- Реализуйте BitmapFactory.Options с inSampleSize для изменения размера изображений “на лету” перед загрузкой битмапов в память
- Используйте библиотеки, такие как Picasso, для автоматического изменения размера, кэширования и управления памятью
- Правильно перерабатывайте битмапы, когда они больше не нужны, для предотвращения утечек памяти
- Рассмотрите возможность кэширования измененных изображений во внешнем хранилище, если ограничения памяти являются серьезными
- Применяйте лучшие практики, такие как паттерн ViewHolder и ленивая загрузка, для оптимальной производительности
Следуя этим стратегиям, вы можете поддерживать хорошее качество изображений, избегая сбоев памяти в вашей реализации ListView. Начните с решения по изменению размера “на лету”, так как оно напрямую отвечает на вашу необходимость в построении адаптера строки за строкой с возможностью изменения изображений, и рассмотрите реализацию внешнего кэширования, если вы продолжаете испытывать проблемы с памятью.