java.lang.OutOfMemoryError: rozmiar mapy bitowej przekracza budżet maszyny wirtualnej - Android

159

Stworzyłem aplikację, która wykorzystuje wiele obrazów na Androida.

Aplikacja działa raz, wypełnia informacje na ekranie ( Layouts, Listviews, Textviews, ImageViews, etc) i użytkownik odczytuje informacje.

Nie ma animacji, żadnych efektów specjalnych ani niczego, co mogłoby wypełnić pamięć. Czasami przedmioty do rysowania mogą się zmienić. Niektóre z nich to zasoby Androida, a niektóre to pliki zapisane w folderze na karcie SDCARD.

Następnie użytkownik kończy pracę ( onDestroymetoda jest wykonywana, a aplikacja pozostaje w pamięci przez maszynę wirtualną), a następnie w pewnym momencie użytkownik wchodzi ponownie.

Za każdym razem, gdy użytkownik wchodzi do aplikacji, widzę, jak pamięć rośnie coraz bardziej, aż użytkownik otrzyma plik java.lang.OutOfMemoryError.

Jaki jest więc najlepszy / prawidłowy sposób obsługi wielu obrazów?

Czy powinienem umieścić je w metodach statycznych, aby nie były ładowane przez cały czas? Czy muszę w specjalny sposób czyścić układ lub obrazy użyte w układzie?

Daniel Benedykt
źródło
4
Może to pomóc, jeśli masz wiele zmian do rysowania. Działa, ponieważ sam zrobiłem program :) androidactivity.wordpress.com/2011/09/24/…

Odpowiedzi:

72

Wygląda na to, że masz wyciek pamięci. Problem nie dotyczy obsługi wielu obrazów, polega na tym, że obrazy nie są zwalniane po zniszczeniu aktywności.

Trudno powiedzieć, dlaczego tak się dzieje, nie patrząc na swój kod. Jednak ten artykuł zawiera kilka wskazówek, które mogą pomóc:

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

W szczególności użycie zmiennych statycznych może pogorszyć sytuację, a nie poprawić. Może zajść potrzeba dodania kodu, który usuwa wywołania zwrotne podczas ponownego rysowania aplikacji - ale znowu nie ma tu wystarczających informacji, aby powiedzieć na pewno.

Trevor Johns
źródło
8
to prawda, a także posiadanie dużej liczby obrazów (a nie wycieku pamięci) może powodować outOfMemory z wielu powodów, takich jak wiele innych aplikacji, fragmentacja pamięci itp. Dowiedziałem się, że podczas pracy z dużą ilością obrazów, jeśli otrzymasz outOfMemory systematycznie, jak zawsze to samo, a potem wyciek. Jeśli dostajesz to jak raz dziennie, czy coś, to dlatego, że jesteś zbyt blisko limitu. Moja sugestia w tym przypadku to spróbuj usunąć część rzeczy i spróbuj ponownie. Również pamięć obrazu jest poza pamięcią sterty, więc zwróć uwagę na oba.
Daniel Benedykt
15
Jeśli podejrzewasz wyciek pamięci, szybkim sposobem monitorowania użycia sterty jest użycie polecenia adb shell dumpsys meminfo. Jeśli zauważysz wzrost użycia sterty w ciągu kilku cykli GC (wiersz dziennika GC_ * w logcat), możesz być prawie pewien, że masz wyciek. Następnie utwórz zrzut sterty (lub kilka w różnym czasie) za pomocą adb lub DDMS i przeanalizuj go za pomocą narzędzia drzewa dominatora w Eclipse MAT. Wkrótce powinieneś dowiedzieć się, który obiekt ma stertę zachowaną, która rośnie z czasem. To twój wyciek pamięci. Napisałem artykuł z kilkoma szczegółami tutaj: macgyverdev.blogspot.com/2011/11/…
Johan Norén
98

Jednym z najczęstszych błędów, które znalazłem podczas tworzenia aplikacji na Androida, jest błąd „java.lang.OutOfMemoryError: rozmiar mapy bitowej przekracza budżet maszyny wirtualnej”. Znalazłem ten błąd często w działaniach wykorzystujących wiele bitmap po zmianie orientacji: działanie jest niszczone, tworzone ponownie, a układy są „zawyżane” z pliku XML zużywającego pamięć maszyny wirtualnej dostępną dla map bitowych.

Mapy bitowe w poprzednim układzie działania nie są prawidłowo usuwane przez moduł odśmiecania pamięci, ponieważ krzyżowały się z nimi odniesienia do ich działania. Po wielu eksperymentach znalazłem całkiem dobre rozwiązanie tego problemu.

Najpierw ustaw atrybut „id” w widoku nadrzędnym układu 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"
     >
     ...

Następnie w onDestroy() metodzie swojego działania wywołaj unbindDrawables()metodę przekazującą odwołanie do widoku nadrzędnego, a następnie wykonaj 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();
        }
    }

Ta unbindDrawables()metoda bada drzewo widoków rekurencyjnie i:

  1. Usuwa wywołania zwrotne ze wszystkich wad w tle
  2. Usuwa elementy podrzędne w każdej grupie widoków
hp.android
źródło
10
Z wyjątkiem ... java.lang.UnsupportedOperationException: removeAllViews () nie jest obsługiwana w AdapterView
Dzięki temu pomaga to znacznie zmniejszyć rozmiar mojej sterty, ponieważ mam wiele elementów do rysowania ... @GJTorikian w tym celu po prostu spróbuj złapać w removeAllViews () większość podklas ViewGroup działa dobrze.
Eric Chen
5
Lub po prostu zmień warunek: if (wyświetl instancję ViewGroup &&! (Wyświetl instancję AdapterView))
Anke
2
@Adam Varhegyi, możesz jawnie wywołać tę samą funkcję dla widoku galerii w onDestroy. Podobnie jak unbindDrawables (galleryView); mam nadzieję, że to działa dla Ciebie ...
hp.android
1
uważaj na stackoverflow tych dwóch
gotcha.com/questions/8405475/…
11

Aby uniknąć tego problemu, możesz użyć metody natywnej Bitmap.recycle()przed null-ing Bitmapobiektu (lub ustawić inną wartość). Przykład:

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

A następnie możesz zmienić bez myBitmapdzwonienia na System.gc()przykład:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
ddmytrenko
źródło
1
to nie zadziała, jeśli spróbujesz dodać te elementy do widoku listy. czy masz jakieś sugestie?
Rafael Sanches
@ddmytrenko Przetwarzasz obraz przed przypisaniem. Czy nie uniemożliwiłoby to renderowania pikseli?
IgorGanapolsky,
8

Natknąłem się dokładnie na ten problem. Sterta jest dość mała, więc te obrazy mogą dość szybko wymknąć się spod kontroli w odniesieniu do pamięci. Jednym ze sposobów jest wskazanie modułowi odśmiecania pamięci, aby zebrał pamięć na mapie bitowej, wywołując jego metodę recycle .

Ponadto wywołanie metody onDestroy nie jest gwarantowane. Możesz przenieść tę logikę / czyszczenie do działania onPause. Zapoznaj się z diagramem / tabelą cyklu życia działania na tej stronie, aby uzyskać więcej informacji.

Donn Felker
źródło
Dzięki za informację. Zarządzam wszystkimi Drawables, a nie Bitmaps, więc nie mogę wywołać Recycle. Jakieś inne sugestie dotyczące rysunków? Dzięki Daniel
Daniel Benedykt
Dzięki za onPausesugestię. Używam 4 zakładek z Bitmapobrazami w każdym z nich, więc zniszczenie aktywności może nie nastąpić zbyt wcześnie (jeśli użytkownik przegląda wszystkie karty).
Azurespot
7

Może pomóc to wyjaśnienie: http://code.google.com/p/android/issues/detail?id=8488#c80

„Szybkie wskazówki:

1) NIGDY nie wywołuj samodzielnie System.gc (). Zostało to propagowane jako poprawka tutaj i nie działa. Nie rób tego. Jeśli zauważyłeś w moim wyjaśnieniu, przed otrzymaniem OutOfMemoryError, JVM już uruchamia czyszczenie pamięci, więc nie ma powodu, aby robić to ponownie (spowalnia to twój program). Wykonanie jednego pod koniec zajęć to tylko zatuszowanie problemu. Może to powodować szybsze umieszczanie mapy bitowej w kolejce finalizatora, ale nie ma powodu, aby zamiast tego po prostu wywołać funkcję recycle na każdej mapie bitowej.

2) Zawsze wywołuj recycle () w przypadku map bitowych, których już nie potrzebujesz. Przynajmniej w onDestroy swojej aktywności przejrzyj i poddaj recyklingowi wszystkie używane mapy bitowe. Ponadto, jeśli chcesz, aby instancje bitmapy były szybciej zbierane ze sterty dalvik, wyczyszczenie jakichkolwiek odniesień do mapy bitowej nie zaszkodzi.

3) Wywołanie recycle (), a następnie System.gc () nadal może nie usunąć mapy bitowej ze stosu Dalvik. NIE PRZESTAWAJ SIĘ tym. recycle () wykonał swoją pracę i zwolnił natywną pamięć, po prostu zajmie trochę czasu, aby przejść przez kroki, które nakreśliłem wcześniej, aby faktycznie usunąć mapę bitową ze stosu Dalvik. To NIE jest wielka sprawa, ponieważ duża część pamięci natywnej jest już wolna!

4) Zawsze zakładaj, że błąd we frameworku jest ostatni. Dalvik robi dokładnie to, co powinien. Może nie być tym, czego oczekujesz lub czego chcesz, ale tak to działa. "

Aron
źródło
5

Miałem dokładnie ten sam problem. Po kilku testach stwierdziłem, że ten błąd pojawia się przy skalowaniu dużego obrazu. Zmniejszyłem skalowanie obrazu i problem zniknął.

PS Na początku próbowałem zmniejszyć rozmiar obrazu bez zmniejszania go. To nie powstrzymało błędu.

GSree
źródło
4
czy mógłbyś opublikować kod, w jaki sposób wykonałeś skalowanie obrazu, mam ten sam problem i myślę, że to może go rozwiązać. Dzięki!
Gligor
@Gix - Myślę, że ma na myśli to, że musiał zmniejszyć rozmiar obrazów przed przeniesieniem ich do zasobów projektu, zmniejszając w ten sposób ślad pamięciowy elementów rysunkowych. W tym celu użyj wybranego edytora zdjęć. Lubię pixlr.com
Kyle Clegg
5

Poniższe punkty bardzo mi pomogły. Mogą być też inne punkty, ale są one bardzo ważne:

  1. Tam, gdzie to możliwe, używaj kontekstu aplikacji (zamiast activity.this).
  2. Zatrzymaj i zwolnij swoje wątki w metodzie działania onPause ()
  3. Zwolnij swoje widoki / wywołania zwrotne w metodzie działania onDestroy ()

źródło
4

Proponuję wygodny sposób rozwiązania tego problemu. Po prostu przypisz wartość atrybutu „android: configChanges”, jak następuje w pliku Mainfest.xml dla błędnej aktywności. lubię to:

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

pierwsze rozwiązanie, które podałem, naprawdę zmniejszyło częstotliwość błędów OOM do niskiego poziomu. Ale to nie rozwiązało całkowicie problemu. A potem podam drugie rozwiązanie:

Jak podano w OOM, zużyłem zbyt dużo pamięci wykonawczej. Więc zmniejszam rozmiar obrazu w ~ / res / drawable mojego projektu. Na przykład przekwalifikowany obraz, który ma rozdzielczość 128X128, może zostać przeskalowany do 64x64, co również byłoby odpowiednie dla mojej aplikacji. A po tym, jak zrobiłem to ze stosem zdjęć, błąd OOM nie występuje ponownie.

Ranger Way
źródło
1
Działa to, ponieważ unikasz ponownego uruchomienia aplikacji. Jest to więc nie tyle rozwiązanie, ile raczej unikanie. Ale czasami to wszystko, czego potrzebujesz. A domyślna metoda onConfigurationChanged () w Activity odwróci twoją orientację, więc jeśli nie potrzebujesz żadnych zmian interfejsu użytkownika, aby naprawdę dostosować się do różnych orientacji, możesz być całkowicie zadowolony z wyniku. Uważaj jednak na inne rzeczy, które mogą Cię ponownie uruchomić. Możesz użyć tego: android: configChanges = "keyboardHidden | Orientation | keyboard | locale | mcc | mnc | touchscreen | screenLayout | fontScale"
Carl
3

Ja też jestem sfrustrowany błędem braku pamięci. I tak, ja też odkryłem, że ten błąd często pojawia się podczas skalowania obrazów. Na początku próbowałem tworzyć rozmiary obrazów dla wszystkich gęstości, ale stwierdziłem, że znacznie zwiększyło to rozmiar mojej aplikacji. Więc teraz używam tylko jednego obrazu dla wszystkich gęstości i skaluję moje obrazy.

Moja aplikacja wyrzucała błąd poza pamięcią za każdym razem, gdy użytkownik przechodził z jednej czynności do drugiej. Ustawienie moich drawables na null i wywołanie System.gc () nie działało, podobnie jak recykling moich obiektów bitmapDrawables za pomocą getBitMap (). Recycle (). Android nadal wyrzucałby błąd poza pamięcią przy pierwszym podejściu i wyrzucałby komunikat o błędzie kanwy za każdym razem, gdy próbowałby użyć odzyskanej mapy bitowej w drugim podejściu.

Podjąłem jeszcze trzecie podejście. Ustawiłem wszystkie widoki na zero, a tło na czarne. Robię to czyszczenie w mojej metodzie onStop (). Jest to metoda, która jest wywoływana, gdy działanie nie jest już widoczne. Jeśli zrobisz to w metodzie onPause (), użytkownicy zobaczą czarne tło. Nieidealny. Jeśli chodzi o zrobienie tego w metodzie onDestroy (), nie ma gwarancji, że zostanie wywołana.

Aby zapobiec pojawieniu się czarnego ekranu, jeśli użytkownik naciśnie przycisk Wstecz na urządzeniu, ponownie ładuję aktywność w metodzie onRestart (), wywołując metody startActivity (getIntent ()), a następnie finish ().

Uwaga: zmiana tła na czarne nie jest naprawdę konieczna.

ujarzmiona radość
źródło
1

Metody BitmapFactory.decode *, omówione w lekcji Skutecznie ładuj duże mapy bitowe , nie powinny być wykonywane w głównym wątku interfejsu użytkownika, jeśli dane źródłowe są odczytywane z dysku lub lokalizacji sieciowej (lub w rzeczywistości dowolnego źródła innego niż pamięć). Czas ładowania tych danych jest nieprzewidywalny i zależy od wielu czynników (szybkość odczytu z dysku lub sieci, rozmiar obrazu, moc procesora itp.). Jeśli jedno z tych zadań blokuje wątek interfejsu użytkownika, system oznacza aplikację jako niereagującą, a użytkownik ma możliwość jej zamknięcia (więcej informacji można znaleźć w sekcji Projektowanie pod kątem responsywności).

Li3ro
źródło
0

Cóż, wypróbowałem wszystko, co znalazłem w Internecie i żaden z nich nie działał. Wywołanie System.gc () tylko zmniejsza prędkość aplikacji. Recykling bitmap w onDestroy też nie działał.

Jedyne, co teraz działa, to mieć statyczną listę wszystkich bitmap, aby mapy bitowe przetrwały po ponownym uruchomieniu. I po prostu użyj zapisanych bitmap zamiast tworzyć nowe za każdym razem, gdy działanie zostanie ponownie uruchomione.

W moim przypadku kod wygląda tak:

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;
    }
}
HarryHao
źródło
czy to nie ujawniłoby twojego kontekstu? (dane dotyczące działalności)
Rafael Sanches,
0

Miałem ten sam problem z przełączaniem obrazów tła na rozsądne rozmiary. Osiągnąłem lepsze wyniki, ustawiając ImageView na null przed umieszczeniem nowego obrazu.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
Gunnar Bernstein
źródło
0

FWIW, oto lekka pamięć podręczna bitmap, którą zakodowałem i używam przez kilka miesięcy. To nie są wszystkie dzwonki i gwizdki, więc przeczytaj kod, zanim go użyjesz.

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

}
Guy Smith
źródło