java.lang.OutOfMemoryError: tamanho do bitmap excede o orçamento da VM - Android

159

Eu desenvolvi um aplicativo que usa muitas imagens no Android.

O aplicativo é executado uma vez, preenche a informação na tela ( Layouts, Listviews, Textviews, ImageViews, etc) e usuário lê as informações.

Não há animação, efeitos especiais ou qualquer coisa que possa preencher a memória. Às vezes, os drawables podem mudar. Alguns são recursos do Android e outros são arquivos salvos em uma pasta no SDCARD.

Em seguida, o usuário sai (o onDestroymétodo é executado e o aplicativo permanece na memória pela VM) e, em algum momento, o usuário entra novamente.

Cada vez que o usuário entra no aplicativo, posso ver a memória crescendo cada vez mais até que o usuário obtenha o java.lang.OutOfMemoryError.

Então, qual é a melhor / maneira correta de lidar com muitas imagens?

Devo colocá-los em métodos estáticos para que eles não sejam carregados o tempo todo? Preciso limpar o layout ou as imagens usadas no layout de uma maneira especial?

Daniel Benedykt
fonte
4
Isso pode ajudar se você tiver muitas drawables alteradas. Está funcionando desde que eu mesmo fiz o programa :) androidactivity.wordpress.com/2011/09/24/…

Respostas:

72

Parece que você tem um vazamento de memória. O problema não é lidar com muitas imagens, é que elas não são desalocadas quando sua atividade é destruída.

É difícil dizer por que isso ocorre sem olhar para o seu código. No entanto, este artigo tem algumas dicas que podem ajudar:

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

Em particular, o uso de variáveis ​​estáticas provavelmente tornará as coisas piores, não melhores. Pode ser necessário adicionar código que remova retornos de chamada quando o aplicativo redesenhar - mas, novamente, não há informações suficientes aqui para garantir.

Trevor Johns
fonte
8
isso é verdade e também ter muitas imagens (e não um vazamento de memória) pode causar outOfMemory por vários motivos, como muitos outros aplicativos em execução, fragmentação de memória etc. Aprendi que, ao trabalhar com muitas imagens, você obtém o outOfMemory sistematicamente, como fazer sempre o mesmo, então é um vazamento. Se você conseguir uma vez por dia ou algo assim, é porque você está muito perto do limite. Minha sugestão nesse caso é tentar remover algumas coisas e tentar novamente. A memória de imagem também está fora da memória Heap, portanto, preste atenção nos dois.
Daniel Benedykt
15
Se você suspeitar de vazamento de memória, uma maneira rápida de monitorar o uso de heap é através do comando adb shell dumpsys meminfo. Se você vir o uso de heap aumentando ao longo de alguns ciclos de gc (linha de log GC_ * no logcat), pode ter certeza de que há um vazamento. Em seguida, crie um dump de heap (ou vários em momentos diferentes) via adb ou DDMS e analise-o através das ferramentas da árvore dominadora no Eclipse MAT. Em breve, você deve descobrir qual objeto tem um heap de retenções que cresce com o tempo. Esse é o seu vazamento de memória. Eu escrevi um artigo com mais alguns detalhes aqui: macgyverdev.blogspot.com/2011/11/...
Johan Norén
98

Um dos erros mais comuns que encontrei no desenvolvimento de aplicativos Android é o erro "java.lang.OutOfMemoryError: tamanho do bitmap excede o orçamento da VM". Encontrei esse erro frequentemente em atividades que usam muitos bitmaps após alterar a orientação: a atividade é destruída, criada novamente e os layouts são "inflados" a partir do XML, consumindo a memória da VM disponível para bitmaps.

Os bitmaps no layout de atividade anterior não são desalocados corretamente pelo coletor de lixo porque eles cruzaram as referências à sua atividade. Depois de muitas experiências, encontrei uma solução muito boa para esse problema.

Primeiro, defina o atributo "id" na visualização pai do seu layout 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"
     >
     ...

Em seguida, no onDestroy() método da sua Atividade, chame o unbindDrawables()método que passa uma referência para a Visualização pai e faça 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();
        }
    }

Este unbindDrawables()método explora a árvore de exibição recursivamente e:

  1. Remove retornos de chamada em todos os drawables em segundo plano
  2. Remove crianças em todos os grupos de visualizações
hp.android
fonte
10
Exceto ... java.lang.UnsupportedOperationException: removeAllViews () não é suportado no AdapterView
Obrigado, isso ajuda a reduzir significativamente o tamanho do meu heap, já que tenho muitas gavetas ...
Eric Chen
5
Ou apenas mudar a condição: if (vista instanceof ViewGroup && (vista instanceof AdapterView)!)
Anke
2
@ Adam Varhegyi, você pode chamar explicitamente a mesma função para visualização de galeria no onDestroy. Como unbindDrawables (galleryView); espero que isso funcione para você ...
hp.android
1
esteja atento a esses dois problemas de stackoverflow.com/questions/8405475/… AND stackoverflow.com/questions/4486034/…
max4ever
11

Para evitar esse problema, você pode usar método nativo Bitmap.recycle()antes nulling Bitmapobjeto (ou estabelecendo outro valor). Exemplo:

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

E a seguir, você pode alterar a myBitmapchamada sem System.gc():

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
ddmytrenko
fonte
1
isso não funcionará se você tentar adicionar esses elementos em uma exibição de lista. você tem alguma sugestão sobre isso?
Rafael Sanches
@ddmytrenko Você está reciclando a imagem antes de atribuí-la. Isso não impediria a renderização de seus pixels?
IgorGanapolsky 28/03
8

Eu encontrei esse problema exato. A pilha é muito pequena, portanto essas imagens podem ficar fora de controle rapidamente em relação à memória. Uma maneira é dar ao coletor de lixo uma dica para coletar memória em um bitmap, chamando seu método de reciclagem .

Além disso, não é garantido que o método onDestroy seja chamado. Você pode mover essa lógica / limpeza para a atividade onPause. Confira o diagrama / tabela do Ciclo de Vida das Atividades nesta página para obter mais informações.

Donn Felker
fonte
Obrigado pela informação. Estou gerenciando todos os Drawables e não os Bitmaps, por isso não posso ligar para Reciclar. Alguma outra sugestão para drawables? Obrigado Daniel
Daniel Benedykt
Obrigado pela onPausesugestão. Estou usando quatro guias com Bitmapimagens em todas elas, portanto, a destruição da atividade pode não acontecer tão cedo (se o usuário navegar em todas as guias).
Azurespot 23/03
7

Esta explicação pode ajudar: http://code.google.com/p/android/issues/detail?id=8488#c80

"Dicas rápidas:

1) NUNCA ligue para System.gc (). Isso foi propagado como uma correção aqui e não funciona. Não faça isso. Se você notou na minha explicação, antes de obter um OutOfMemoryError, a JVM já executa uma coleta de lixo, portanto não há razão para fazer uma novamente (isso está atrasando o programa). Fazer uma no final de sua atividade está apenas encobrindo o problema. Isso pode fazer com que o bitmap seja colocado na fila do finalizador mais rapidamente, mas não há motivo para você não poder simplesmente chamar de reciclar em cada bitmap.

2) Sempre chame recycle () em bitmaps que você não precisa mais. No mínimo, na destruição de sua atividade, repasse e recicle todos os bitmaps que você estava usando. Além disso, se você quiser que as instâncias de bitmap sejam coletadas do heap do dalvik mais rapidamente, não será necessário limpar nenhuma referência ao bitmap.

3) Chamar recycle () e System.gc () ainda podem não remover o bitmap da pilha Dalvik. NÃO SE PREOCUPE com isso. O Recycle () fez seu trabalho e liberou a memória nativa. Levará algum tempo para seguir as etapas descritas anteriormente para remover o bitmap da pilha Dalvik. Isso NÃO é grande coisa porque a grande parte da memória nativa já está livre!

4) Sempre assuma que há um erro na estrutura por último. Dalvik está fazendo exatamente o que deveria fazer. Pode não ser o que você espera ou o que deseja, mas é como funciona. "

Aron
fonte
5

Eu tive o mesmo problema. Após alguns testes, descobri que esse erro está aparecendo para o dimensionamento de imagens grandes. Reduzi a escala da imagem e o problema desapareceu.

PS No começo, tentei reduzir o tamanho da imagem sem reduzi-la. Isso não parou o erro.

GSree
fonte
4
você pode postar o código de como você fez o dimensionamento da imagem, estou enfrentando o mesmo problema e acho que isso pode resolvê-lo. Obrigado!
Gligor 27/05
@ Gix - eu acredito que ele quer dizer que ele teve que reduzir o tamanho das imagens antes de trazê-las para os recursos do projeto, diminuindo assim a pegada de memória dos drawables. Para isso, você deve usar o editor de fotos de sua escolha. Eu gosto de pixlr.com
Kyle Clegg
5

Os seguintes pontos realmente me ajudaram muito. Pode haver outros pontos também, mas estes são muito cruciais:

  1. Use o contexto do aplicativo (em vez de activity.this) sempre que possível.
  2. Pare e libere seus threads no método de atividade onPause ()
  3. Libere suas visualizações / retornos de chamada no método de atividade onDestroy ()

fonte
4

Sugiro uma maneira conveniente de resolver esse problema. Basta atribuir o valor do atributo "android: configChanges", conforme seguido no Mainfest.xml, para a sua atividade com erro. como isso:

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

a primeira solução apresentada realmente reduziu a frequência do erro OOM para um nível baixo. Mas, não resolveu o problema totalmente. E então eu vou dar a segunda solução:

Conforme detalhado no OOM, usei muita memória de tempo de execução. Portanto, reduzo o tamanho da imagem em ~ / res / drawable do meu projeto. Como uma imagem superqualificada com resolução de 128X128, pode ser redimensionada para 64x64, o que também seria adequado para a minha aplicação. E depois que fiz isso com uma pilha de fotos, o erro OOM não ocorre novamente.

Ranger Way
fonte
1
Isso funciona porque você está evitando a reinicialização do seu aplicativo. Portanto, não é tanto uma solução, mas uma evitação. Mas, às vezes, é tudo o que você precisa. E o método onConfigurationChanged () padrão no Activity alterará sua orientação para você; portanto, se você não precisar de alterações na interface do usuário para se adaptar verdadeiramente às diferentes orientações, poderá ficar perfeitamente satisfeito com o resultado. Cuidado com outras coisas que podem reiniciar você, no entanto. Convém usar isso: android: configChanges = "keyboardHidden | orientação | teclado | local | | mcc | mnc | tela sensível ao toque | screenLayout | fontScale"
Carl
3

Eu também estou frustrado com o bug fora da memória. E sim, também descobri que esse erro aparece muito ao dimensionar imagens. No começo, tentei criar tamanhos de imagem para todas as densidades, mas achei que isso aumentava substancialmente o tamanho do meu aplicativo. Então, agora estou usando apenas uma imagem para todas as densidades e dimensionando minhas imagens.

Meu aplicativo gerava um erro fora da memória sempre que o usuário passava de uma atividade para outra. Definir meus drawables como nulos e chamar System.gc () não funcionou, nem reciclar meus bitmapDrawables com getBitMap (). Recycle (). O Android continuaria lançando o erro fora da memória com a primeira abordagem e lançaria uma mensagem de erro de tela sempre que tentasse usar um bitmap reciclado com a segunda abordagem.

Fiz uma terceira abordagem. Defino todas as visualizações como nulas e o fundo como preto. Eu faço essa limpeza no meu método onStop (). Este é o método chamado assim que a atividade não estiver mais visível. Se você fizer isso no método onPause (), os usuários verão um fundo preto. Não é ideal. Quanto a fazê-lo no método onDestroy (), não há garantia de que será chamado.

Para impedir que uma tela preta ocorra se o usuário pressionar o botão Voltar no dispositivo, recarrego a atividade no método onRestart () chamando os métodos startActivity (getIntent ()) e depois concluo ().

Nota: não é realmente necessário alterar o fundo para preto.

alegria
fonte
1

Os métodos BitmapFactory.decode *, discutidos na lição Carregar bitmaps grandes com eficiência , não devem ser executados no thread principal da interface do usuário se os dados de origem forem lidos no disco ou em um local de rede (ou realmente em qualquer outra fonte que não seja a memória). O tempo que esses dados levam para carregar é imprevisível e depende de vários fatores (velocidade de leitura do disco ou da rede, tamanho da imagem, potência da CPU, etc.). Se uma dessas tarefas bloquear o encadeamento da interface do usuário, o sistema sinalizará seu aplicativo como não responsivo e o usuário terá a opção de fechá-lo (consulte Designing for Responsiveness para obter mais informações).

Li3ro
fonte
0

Bem, eu tentei tudo o que encontrei na internet e nenhum deles funcionou. Chamar System.gc () apenas reduz a velocidade do aplicativo. A reciclagem de bitmaps no onDestroy também não funcionou para mim.

A única coisa que funciona agora é ter uma lista estática de todo o bitmap para que os bitmaps sobrevivam após uma reinicialização. E use os bitmaps salvos em vez de criar novos sempre que a atividade for reiniciada.

No meu caso, o código fica assim:

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
fonte
isso não vazaria seu contexto? (dados da atividade)
Rafael Sanches
0

Eu tive o mesmo problema apenas ao mudar as imagens de fundo com tamanhos razoáveis. Obtive melhores resultados ao definir o ImageView como nulo antes de colocar uma nova imagem.

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

FWIW, aqui está um cache de bitmap leve que eu codifiquei e usei por alguns meses. Não são todos os sinos e assobios, portanto, leia o código antes de usá-lo.

/**
 * 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
fonte