Java.lang.OutOfMemoryError: размер растрового изображения превышает бюджет VM - Android

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

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

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

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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

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

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

Сначала установите атрибут "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() исследует дерево представления рекурсивно и:

  • Удаляет обратные вызовы во всех фоновых рисунках
  • Удаляет дочерние элементы в каждой группе просмотра

Ответ 3

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

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

И затем вы можете изменить myBitmap без вызова System.gc():

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

Ответ 4

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

  • Используйте контекст приложения (а не activity.this), где это возможно.
  • Остановите и отпустите свои потоки в onpause() методе деятельности
  • Отпустите свои виды/обратные вызовы в onDestroy() методе деятельности

Ответ 8

Я предлагаю удобный способ решить эту проблему. Просто присвойте атрибуту "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 не повторится.

Ответ 9

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

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

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

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

Примечание: нет необходимости менять фон на черный.

Ответ 10

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

Ответ 11

Ну, я пробовал все, что нашел в Интернете, и никто из них не работал. Вызов 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;
    }
}

Ответ 12

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

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

Ответ 13

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

/**
 * 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 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 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();
    }

}