java.lang.OutOfMemoryError: размер растрового изображения превышает бюджет виртуальной машины - Android

Я разработал приложение, которое использует много изображений на Android.

Приложение запускается один раз, заполняет информацию на экране (Layouts, Listviews, Textviews, ImageViewsи т. д.) и пользователь читает информацию.

Там нет анимации, никаких спецэффектов или чего-либо, что может заполнить память. Иногда ничья может измениться. Некоторые из них являются ресурсами Android, а некоторые файлы сохраняются в папке на SDCARD.

Затем пользователь выходит (onDestroy метод выполняется, и приложение остается в памяти виртуальной машиной), а затем в какой-то момент пользователь входит снова.

Каждый раз, когда пользователь входит в приложение, я вижу, как увеличивается и увеличивается объем памяти, пока пользователь не получит java.lang.OutOfMemoryError,

Так какой же самый лучший / правильный способ обработки множества изображений?

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

13 ответов

Решение

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

Трудно сказать, почему это не глядя на ваш код. Однако в этой статье есть несколько советов, которые могут помочь:

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

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

Одной из самых распространенных ошибок, которые я обнаружил при разработке Android-приложений, является ошибка "java.lang.OutOfMemoryError: Размер растрового изображения превышает бюджет виртуальной машины". Я часто обнаруживал эту ошибку в действиях, использующих большое количество растровых изображений после изменения ориентации: действие уничтожается, создается заново, а макеты "раздуваются" из XML, потребляя память виртуальной машины, доступную для растровых изображений.

Битовые карты в предыдущем макете действий неправильно распределяются сборщиком мусора, поскольку они пересекли ссылки на свои действия. После многих экспериментов я нашел довольно хорошее решение для этой проблемы.

Сначала установите атрибут "id" в родительском представлении макета XML:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

Затем на onDestroy() метод вашей деятельности, позвоните unbindDrawables() метод, передавая ссылку на родительский вид, а затем сделать System.gc(),

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

это unbindDrawables() Метод рекурсивно исследует дерево представлений и:

  1. Удаляет обратные вызовы на всех фоновых рисунках
  2. Удаляет детей в каждой группе

Чтобы избежать этой проблемы вы можете использовать нативный метод Bitmap.recycle() до null-ную Bitmap объект (или установка другого значения). Пример:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

И рядом вы можете изменить myBitmap без звонка System.gc() лайк:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

Я столкнулся с этой точной проблемой. Куча довольно маленькая, поэтому эти изображения могут довольно быстро выйти из-под контроля в отношении памяти. Один из способов - дать сборщику мусора подсказку собирать память на растровом изображении, вызвав его метод recycle.

Кроме того, метод onDestroy не гарантированно вызывается. Возможно, вы захотите перенести эту логику / очистить в действие onPause. Посмотрите диаграмму / таблицу жизненного цикла активности на этой странице для получения дополнительной информации.

Это объяснение может помочь: http://code.google.com/p/android/issues/detail?id=8488

"Быстрые советы:

1) НИКОГДА не вызывайте System.gc() самостоятельно. Это было распространено как исправление здесь, и это не работает. Не делай этого. Если вы заметили в моем объяснении, перед тем как получить OutOfMemoryError, JVM уже запускает сборку мусора, поэтому нет причин делать это снова (это замедляет работу вашей программы). Выполнение одного в конце вашей деятельности просто прикрывает проблему. Это может привести к тому, что растровое изображение будет помещено в очередь финализатора быстрее, но нет причины, по которой вы не могли бы просто вызвать recycle для каждого растрового изображения.

2) Всегда вызывайте recycle() для растровых изображений, которые вам больше не нужны. По крайней мере, в onDestroy вашей деятельности выполните и переработайте все растровые изображения, которые вы использовали. Кроме того, если вы хотите, чтобы экземпляры растрового изображения собирались из кучи dalvik быстрее, не повредит очистить любые ссылки на растровое изображение.

3) Вызов recycle(), а затем System.gc() все еще не может удалить растровое изображение из кучи Dalvik. НЕ ЗАБУДЬТЕ об этом. recycle() выполнил свою работу и освободил родную память, просто потребуется некоторое время, чтобы пройти шаги, которые я описал ранее, чтобы фактически удалить растровое изображение из кучи Dalvik. Это не имеет большого значения, потому что большой кусок родной памяти уже свободен!

4) Всегда предполагайте, что в фреймворке последняя ошибка. Dalvik делает именно то, что должен делать. Это может быть не то, что вы ожидаете или чего вы хотите, а то, как это работает. "

Следующие пункты очень помогли мне. Могут быть и другие моменты, но они очень важны:

  1. Используйте контекст приложения (а не Activity.this), где это возможно.
  2. Остановите и освободите ваши темы в методе активности onPause()
  3. Отпустите ваши представления / обратные вызовы в методе действия onDestroy()

У меня была точно такая же проблема. После нескольких испытаний я обнаружил, что эта ошибка появляется при масштабировании большого изображения. Я уменьшил масштаб изображения, и проблема исчезла.

PS Сначала я попытался уменьшить размер изображения, не уменьшая его. Это не остановило ошибку.

Я предлагаю удобный способ решения этой проблемы. Просто присвойте атрибуту "android:configChanges" значение, как указано в файле Mainfest.xml, для действия с ошибками. как это:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

первое решение, которое я дал, действительно уменьшило частоту ошибок OOM до низкого уровня. Но это не решило проблему полностью. И тогда я выдам 2-е решение:

Как подробно описал OOM, я использовал слишком много оперативной памяти. Итак, я уменьшил размер изображения в ~/res/drawable моего проекта. Например, переоцененное изображение с разрешением 128х128 можно изменить до 64х64, что также подойдет для моего приложения. И после того, как я сделал это с кучей картинок, ошибка OOM больше не возникает.

Я тоже разочарован ошибкой вне памяти. И да, я также обнаружил, что эта ошибка часто появляется при масштабировании изображений. Сначала я попытался создать размеры изображения для всех плотностей, но обнаружил, что это существенно увеличило размер моего приложения. Так что теперь я просто использую одно изображение для всех плотностей и масштабирую свои изображения.

Мое приложение будет выдавать ошибку вне памяти всякий раз, когда пользователь переходит от одного действия к другому. Установка для моих drawables значения null и вызов System.gc() не сработали, равно как и переработка моих bitmapDrawables с помощью getBitMap(). Recycle(). Android будет продолжать выдавать ошибку "outofmemory" при первом подходе, и он будет выдавать сообщение об ошибке "canvas" всякий раз, когда будет пытаться использовать переработанное растровое изображение со вторым подходом.

Я выбрал даже третий подход. Я установил все виды на ноль, а фон черный. Я делаю эту очистку в моем методе onStop(). Этот метод вызывается, как только действие перестает быть видимым. Если вы сделаете это в методе onPause(), пользователи увидят черный фон. Не идеально. Что касается этого в методе onDestroy(), нет гарантии, что он будет вызван.

Чтобы предотвратить появление черного экрана, если пользователь нажимает кнопку "Назад" на устройстве, я перезагружаю действие в методе onRestart(), вызывая методы startActivity(getIntent()) и затем finish().

Примечание: не обязательно менять фон на черный.

Методы BitmapFactory.decode*, описанные в уроке " Загрузка больших растровых изображений эффективно", не должны выполняться в главном потоке пользовательского интерфейса, если исходные данные считываются с диска или из сетевого расположения (или действительно из любого источника, кроме памяти). Время загрузки этих данных непредсказуемо и зависит от множества факторов (скорость чтения с диска или сети, размер изображения, мощность процессора и т. Д.). Если одна из этих задач блокирует поток пользовательского интерфейса, система помечает ваше приложение как неотвечающее, и пользователь может закрыть его (для получения дополнительной информации см. Проектирование для отзывчивости).

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

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}

Ну, я перепробовал все, что нашел в интернете, и ни один из них не сработал. Вызов System.gc() только снижает скорость приложения. Утилизация растровых изображений в onDestroy не работает для меня тоже.

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

В моем случае код выглядит так:

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

У меня была такая же проблема только с переключением фоновых изображений с разумными размерами. Я получил лучшие результаты, установив для ImageView значение NULL, прежде чем вставлять новое изображение.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
Другие вопросы по тегам