java.lang.OutOfMemoryError: розмір растрових зображень перевищує бюджет VM - Android


159

Я розробив додаток, який використовує безліч зображень на Android.

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

Немає анімації, спеціальних ефектів чи нічого, що може заповнити пам'ять. Іноді малюнки можуть змінюватися. Деякі - це ресурси Android, а деякі - файли, збережені у папці SDCARD.

Потім користувач виходить ( onDestroyметод виконується і додаток залишається в пам'яті VM), а потім в якийсь момент користувач знову входить.

Кожен раз, коли користувач заходить у додаток, я бачу, як пам'ять зростає все більше і більше, поки користувач не отримає 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 систематично, як завжди робити те саме, то це його витік. Якщо ви отримуєте це як раз на день чи щось таке, це тому, що ви занадто близько до межі. Моя пропозиція в цьому випадку - спробувати видалити деякі речі та спробувати ще раз. Також пам'ять зображень знаходиться поза пам'яттю Heap, тому пильнуйте обох.
Даніель Бенедикт

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

98

Однією з найпоширеніших помилок, яку я виявив при розробці програм для Android, є помилка "java.lang.OutOfMemoryError: Розмір бітової карти перевищує бюджет VM". Я часто виявляв цю помилку в діяльності, що використовує багато растрових зображень після зміни орієнтації: активність знищується, створюється знову, а макети «надуваються» від 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()методу, який передає посилання на батьківський вигляд, а потім зробіть 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 для цього просто спробуйте впіймати на RemoveAllViews (), більшість підкласів ViewGroup працює чудово.
Ерік Чен

5
Або просто змінити умову: якщо (переглянути instanceof ViewGroup &&! (Переглянути instanceof AdapterView))
Anke

2
@Adam Varhegyi, ви можете явно викликати ту саму функцію для перегляду галереї в onDestroy. Як unbindDrawables (galleryView); сподіваюся, що це працює для вас ...
hp.android


11

Щоб уникнути цієї проблеми, ви можете використовувати нативний метод 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);

1
це не спрацює, якщо спробувати додати ці елементи до перегляду списку. Чи є у вас якісь пропозиції щодо цього?
Рафаель Санчес

@ddmytrenko Ви переробляєте зображення перед призначенням. Хіба це не завадить рендексувати його пікселі?
ІгорГанапольський

8

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

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


Дякуємо за інформацію. Я керую всіма Drawables, а не Bitmaps, тому я не можу зателефонувати Recycle. Будь-яка інша пропозиція щодо товарних предметів? Дякую Даниїлу
Даніелю Бенедикту

Дякую за onPauseпропозицію. Я використовую 4 вкладки із Bitmapзображеннями в усіх, тому знищення активності може не відбутися занадто рано (якщо користувач перегляне всі вкладки).
Azurespot

7

Це пояснення може допомогти: http://code.google.com/p/android/isissue/detail?id=8488#c80

"Швидкі поради:

1) НІКОЛИ не дзвоніть System.gc () самостійно. Це поширюється як виправлення, і це не працює. Не роби цього. Якщо ви помітили в моєму поясненні, перед тим, як отримати OutOfMemoryError, JVM вже запускає збір сміття, тому немає причин робити це ще раз (це сповільнює вашу програму). Робити це в кінці своєї діяльності - це просто прикрити проблему. Це може призвести до швидшого розміщення растрової карти на черзі фіналізатора, але немає причини, щоб ви не могли просто викликати переробку на кожній растровій карті.

2) Завжди дзвоніть recycle () на растрові карти, які вам більше не потрібні. Як мінімум, у програмі onDestroy перегляньте та переробіть усі використані растрові карти. Крім того, якщо ви хочете, щоб екземпляри растрових файлів швидше збиралися з кучі дальвіків, не завадить очистити будь-які посилання на растрову карту.

3) Виклик recycle (), а потім System.gc () все ще не може видалити растрові карти з кучі Dalvik. НЕ ДУМАЙТЕ про це. recycle () зробив свою роботу і звільнив рідну пам’ять, знадобиться лише деякий час, щоб пройти ті кроки, які я окреслив раніше, щоб фактично видалити растровий файл із кучі Дальвіка. Це НЕ велика справа, тому що великий шматок рідної пам’яті вже безкоштовний!

4) Завжди припускайте, що в рамках остання помилка. Далвік робить саме те, що повинен робити. Це може бути не те, чого ви очікуєте, або те, що ви хочете, але це те, як воно працює. "


5

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

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


4
Ви можете опублікувати код про те, як ви зробили масштабування зображення, я зіткнувся з тією ж проблемою, і я думаю, що це може вирішити його. Дякую!
Глігор

@Gix - Я вважаю, що він означає, що йому довелося зменшити розмір зображень перед тим, як вносити їх у проектні ресурси, тим самим знижуючи слід пам'яті чернеток. Для цього слід використовувати редактор фотографій на ваш вибір. Мені подобається pixlr.com
Kyle Clegg

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 / Dravable для мого проекту. Такий, як завищена кваліфікована картина з роздільною здатністю 128X128, може бути змінена до 64x64, що також підходить для мого застосування. І після того, як я це зробив із купою фотографій, помилка OOM більше не виникає.


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

3

Я теж розчарований помилковою помилкою. І так, я теж виявив, що ця помилка вискакує багато під час масштабування зображень. Спочатку я спробував створити розміри зображень для будь-якої щільності, але виявив, що це значно збільшило розмір мого додатка. Тому я зараз використовую лише одне зображення для всіх густин та масштабування своїх зображень.

У моїй програмі виникає помилка перемоги, коли користувач переходив від однієї діяльності до іншої. Налаштування моїх витяжних елементів на null та виклик System.gc () не спрацювало, а також не було перероблено мою растрову графіку за допомогою getBitMap (). Recycle (). Android буде продовжувати викидати помилку спонукання при першому підході, і він буде кидати повідомлення про помилку на полотні, коли б намагався використовувати перероблену растрову карту з другим підходом.

Я взяв навіть третій підхід. Я встановив усі перегляди на нульові, а фон - на чорний. Я роблю це очищення в моєму методі onStop (). Це метод, який викликається, як тільки активність більше не відображається. Якщо це зробити в методі onPause (), користувачі побачать чорний фон. Не ідеально. Що стосується цього методу onDestroy (), немає гарантії, що він буде викликаний.

Щоб запобігти появі чорного екрану, якщо користувач натискає кнопку назад на пристрої, я перезавантажую активність методом onRestart (), викликаючи метод startActivity (getIntent ()), а потім закінчую ().

Примітка: змінити фон на чорний зовсім не потрібно.


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 нульовим перед тим, як розмістити нову картинку.

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

0

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'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.