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


159

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

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

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

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

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

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

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


4
Это может помочь, если у вас есть много сменных элементов. Это работает , так как я сделал программу сам :) androidactivity.wordpress.com/2011/09/24/...

Ответы:


72

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

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

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

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


8
это верно, и наличие большого количества изображений (а не утечки памяти) может вызвать outOfMemory по нескольким причинам, например, из-за запуска множества других приложений, фрагментации памяти и т. д. Я узнал, что при работе с большим количеством изображений, если вы получаете outOfMemory систематически, как делать всегда то же самое, то это утечка. Если вы получаете это как раз в день или что-то, потому что вы слишком близки к пределу. Мое предложение в этом случае - попытаться удалить некоторые вещи и повторить попытку. Также память изображений находится за пределами памяти кучи, так что следите за обоими.
Даниэль Бенедикт

15
Если вы подозреваете утечку памяти, быстрый способ отслеживать использование кучи - с помощью команды adb shell dumpsys meminfo. Если вы видите, что использование кучи увеличивается за несколько циклов gc (строка журнала GC_ * в logcat), вы можете быть уверены, что у вас есть утечка. Затем создайте дамп кучи (или несколько в разное время) через adb или DDMS и проанализируйте его с помощью инструментов дерева доминант в Eclipse MAT. Вскоре вы должны найти, какой объект имеет кучу, которая растет со временем. Это ваша утечка памяти. Я написал статью с более подробной информацией здесь: macgyverdev.blogspot.com/2011/11/…
Йохан Норен

98

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

10
Кроме ... java.lang.UnsupportedOperationException: removeAllViews () не поддерживается в AdapterView

Спасибо, это помогает значительно уменьшить размер моей кучи, так как у меня много отрисовок ... @GJTorikian для этого просто попробуйте catch в removeAllViews (), большинство подклассов ViewGroup работают просто отлично.
Эрик Чен

5
Или просто измените условие: if (просмотр экземпляра ViewGroup &&! (Просмотр экземпляра AdapterView))
Anke

2
@ Adam Varhegyi, вы можете явно вызывать ту же функцию для просмотра галереи в onDestroy. Как unbindDrawables (galleryView); надеюсь, что это работает для вас ...
hp.android

1
следите за этими двумя gotcha's stackoverflow.com/questions/8405475/… И stackoverflow.com/questions/4486034/…
max4ever

11

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

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

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

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
это не сработает, если вы попытаетесь добавить эти элементы в список. у вас есть какие-либо предложения по этому поводу?
Рафаэль Санчес

@ddmytrenko Вы перерабатываете изображение, прежде чем назначить его. Разве это не помешало бы его пикселям рендериться?
Игорь Ганапольский

8

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

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


Спасибо за информацию. Я управляю всеми Drawables, а не растровыми изображениями, поэтому я не могу вызвать Recycle. Любое другое предложение для Drawables? Спасибо Даниэль
Даниэль Бенедикт

Спасибо за onPauseпредложение. Я использую 4 вкладки с Bitmapизображениями во всех них, поэтому уничтожение активности может произойти не слишком быстро (если пользователь просматривает все вкладки).
Azurespot

7

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

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

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

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

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

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


5

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

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


4
Не могли бы вы опубликовать код о том, как вы сделали масштабирование изображения, я столкнулся с той же проблемой, и я думаю, что это может решить ее. Спасибо!
Gligor

@ Gix - я полагаю, что он имеет в виду, что ему пришлось уменьшить размер изображений, прежде чем вводить их в ресурсы проекта, тем самым уменьшая объем памяти для рисования. Для этого вы должны использовать редактор фотографий на ваш выбор. Мне нравится pixlr.com
Кайл Клегг,

5

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

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

4

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


1
Это работает, потому что вы избегаете перезапуска своего приложения. Так что это не столько решение, сколько избегание. Но иногда это все, что вам нужно. И метод onConfigurationChanged () по умолчанию в Activity изменяет вашу ориентацию для вас, поэтому, если вам не нужны какие-либо изменения пользовательского интерфейса, чтобы по-настоящему адаптироваться к различным ориентациям, вы можете быть совершенно довольны результатом. Остерегайтесь других вещей, которые могут перезапустить вас, хотя. Вы можете использовать это: android: configChanges = "клавиатура скрытая | ориентация | клавиатура | локаль | mcc | mnc | сенсорный экран | screenLayout | fontScale"
Карл

3

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

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

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

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

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


1

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


0

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

не утечет ли это из вашего контекста? (данные об активности)
Рафаэль Санчес

0

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

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

0

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

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

}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.