НейроАгент

Исправление OutOfMemoryError в Android ListView при работе с изображениями

Узнайте, как исправить OutOfMemoryError при загрузке изображений в Bitmap в Android ListView. Узнайте о техниках изменения размера в реальном времени, стратегиях управления памятью и лучших практиках для эффективной загрузки изображений в списках.

Как решить проблему OutOfMemoryError при загрузке изображений в Bitmap в Android ListView?

Я разрабатываю Android-приложение с ListView, содержащим кнопки с изображениями в каждой строке. Когда пользователи нажимают на строку списка, запускается новая активность. Я создал пользовательские вкладки из-за проблем с компоновкой камеры. Запущенная активность - это карта, и когда я нажимаю кнопку для запуска предпросмотра изображения (загрузка изображения с SD-карты), приложение возвращается к активности ListView, а затем пытается перезапустить активность предпросмотра изображения.

Предпросмотр изображения в ListView реализован с помощью курсора и ListAdapter. Мне нужно изменять размер изображений (в байтах, а не в пикселях) на лету для атрибута src кнопки с изображением, поэтому я изменяю размер изображений с камеры телефона.

Проблема в том, что я получаю ошибку OutOfMemoryError, когда приложение пытается вернуться и перезапустить вторую активность.

Основные вопросы:

  1. Есть ли способ построить адаптер списка строка за строкой, позволяющий изменять размер изображений на лету?
    Это было бы предпочтительно, так как мне также нужно изменять свойства виджетов в каждой строке из-за проблем с фокусировкой на сенсорном экране (хотя навигация с помощью шарика работает).

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

Отключение изображения в ListView решает проблему, что подтверждает, что проблема связана с загрузкой изображений.

Текущая реализация:

java
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

Ошибка 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. Это позволяет декодировать изображение с меньшим разрешением перед загрузкой в память.

java
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 память из битмапа при необходимости:

java
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);

Однако, согласно результатам исследования, этот метод может быть не всегда надежным, как отмечено в обсуждении Stack Overflow о “Неправильном вызове JPEG-библиотеки в состоянии” ошибках.


Управление памятью и переработка

Правильная переработка битмапов

Как упоминалось в результатах исследования, распространенным решением является “освобождение выделенной памяти при выходе из активности” путем вызова метода recycle() для каждого отображаемого битмапа. Это критически важно для предотвращения утечек памяти:

java
@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() для проактивного освобождения памяти, когда система испытывает нехватку памяти:

java
@Override
public void onLowMemory() {
    super.onLowMemory();
    System.gc(); // очищаем битмапы и другие объекты здесь для уменьшения использования памяти
}

Альтернативные подходы и библиотеки

Использование библиотеки Picasso

Результаты исследования рекомендуют использовать библиотеку Picasso от Square как эффективное решение. Picasso автоматически обрабатывает управление памятью, кэширование и изменение размера:

java
Picasso.with(context)
       .load(url)
       .resize(50, 50)
       .centerCrop()
       .into(imageView);

Этот подход значительно снижает использование памяти за счет изменения размера изображений перед их загрузкой в память. Библиотека также реализует эффективные стратегии кэширования для предотвращения повторной загрузки одних и тех же изображений.

Кэширование на внешнем хранилище

Еще один альтернативный подход, упомянутый в исследованиях, - это “кэшировать загруженные битмапы на внешнем хранилище, таком как SD-карта, и перезагружать их по мере необходимости вместо попытки удерживать все в ОЗУ”. Этот подход включает:

  1. Изменение размера изображений до соответствующих размеров
  2. Сохранение их на внешнее хранилище
  3. Загрузку предварительно измененных изображений при необходимости

Этот метод особенно полезен, если у вас ограниченный объем ОЗУ, но достаточное пространство для хранения.


Примеры реализации

Пользовательский SimpleCursorAdapter с изменением размера “на лету”

Для решения вашей конкретной необходимости в построении адаптера строки за строкой с изменением размера “на лету”, вот полная реализация:

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

Изменение размера и сохранение вне очереди

Для альтернативного подхода, который вы упомянули, вот как реализовать изменение размера и сохранение вне очереди:

java
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, чтобы избежать повторных поисков представлений:

java
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 для кэширования недавно использованных битмапов в памяти:

java
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 или аналогичных механизмов фоновой загрузки:

java
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. Ключ заключается в балансе между качеством изображения и ограничениями памяти путем реализации правильного масштабирования, кэширования и стратегий управления памятью.

Источники

  1. Android Training - Displaying Bitmaps Efficiently - Официальная документация Android по загрузке битмапов и управлению памятью
  2. Strange OutOfMemory issue while loading an image to a Bitmap object - Stack Overflow - Комплексное обсуждение проблем с памятью битмапов и решений
  3. Android Bitmap OutOfMemoryError in ListView - Stack Overflow - Конкретное решение для управления памятью битмапов в ListView
  4. How to avoid OutOfMemoryError - Stack Overflow - Использование библиотеки Picasso и техники оптимизации памяти
  5. Out of memory error: vast bitmap - Stack Overflow - Подход к кэшированию на внешнем хранилище

Заключение

Решение проблемы OutOfMemoryError при загрузке изображений в Android ListView требует комплексного подхода, сочетающего правильное изменение размера изображений “на лету”, эффективное управление памятью и стратегическое кэширование. Ключевые выводы:

  1. Реализуйте BitmapFactory.Options с inSampleSize для изменения размера изображений “на лету” перед загрузкой битмапов в память
  2. Используйте библиотеки, такие как Picasso, для автоматического изменения размера, кэширования и управления памятью
  3. Правильно перерабатывайте битмапы, когда они больше не нужны, для предотвращения утечек памяти
  4. Рассмотрите возможность кэширования измененных изображений во внешнем хранилище, если ограничения памяти являются серьезными
  5. Применяйте лучшие практики, такие как паттерн ViewHolder и ленивая загрузка, для оптимальной производительности

Следуя этим стратегиям, вы можете поддерживать хорошее качество изображений, избегая сбоев памяти в вашей реализации ListView. Начните с решения по изменению размера “на лету”, так как оно напрямую отвечает на вашу необходимость в построении адаптера строки за строкой с возможностью изменения изображений, и рассмотрите реализацию внешнего кэширования, если вы продолжаете испытывать проблемы с памятью.