Como a alocação de bitmap funciona no Oreo e como investigar sua memória?

fundo

Nos últimos anos, para verificar quanta memória heap você tem no Android e quanto usa, você pode usar algo como:

@JvmStatic
fun getHeapMemStats(context: Context): String {
    val runtime = Runtime.getRuntime()
    val maxMemInBytes = runtime.maxMemory()
    val availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory())
    val usedMemInBytes = maxMemInBytes - availableMemInBytes
    val usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes
    return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
            Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)"
}

Isso significa que, quanto mais memória você usa, especialmente armazenando Bitmaps na memória, mais perto fica da memória heap máxima que seu aplicativo pode usar. Quando você atinge o máximo, seu aplicativo falha com a exceção OutOfMemory (OOM).

O problema

Percebi que no Android O (8.1 no meu caso, mas provavelmente também no 8.0), o código acima não é afetado pelas alocações de Bitmap.

Indo mais longe, notei no profiler Android que quanto mais memória você usa (salvando bitmaps grandes no meu POC), mais memória nativa é usada.

Para testar como ele funciona, criei um loop simples como tal:

    val list = ArrayList<Bitmap>()
    Log.d("AppLog", "memStats:" + MemHelper.getHeapMemStats(this))
    useMoreMemoryButton.setOnClickListener {
        AsyncTask.execute {
            for (i in 0..1000) {
                // list.add(Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888))
                list.add(BitmapFactory.decodeResource(resources, R.drawable.huge_image))
                Log.d("AppLog", "heapMemStats:" + MemHelper.getHeapMemStats(this) + " nativeMemStats:" + MemHelper.getNativeMemStats(this))
            }
        }
    }

Em alguns casos, eu fiz em uma única iteração e, em alguns, apenas criei um bitmap na lista, em vez de decodificá-lo (código no comentário). Mais sobre isso mais tarde ...

Este é o resultado da execução do acima:

Como você pode ver no gráfico, o aplicativo alcançou um grande uso de memória, bem acima da memória heap máxima permitida que foi relatada para mim (que é de 201 MB).

O que eu encontrei

Eu encontrei muitos comportamentos estranhos. Por isso, decidi relatar sobre eles,aqui.

Primeiro, tentei uma alternativa ao código acima, para obter as estatísticas de memória em tempo de execução:

 @JvmStatic
 fun getNativeMemStats(context: Context): String {
     val nativeHeapSize = Debug.getNativeHeapSize()
     val nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
     val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
     val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
     return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
             Formatter.formatShortFileSize(context, nativeHeapSize) + " (" + usedMemInPercentage + "%)"
 }

Mas, ao contrário da verificação da memória heap, parece que a memória nativa máxima altera seu valor ao longo do tempo, o que significa que não posso saber qual é o seu valor realmente máximo e, portanto, não posso, em aplicativos reais, decidir tamanho do cache de memória deve ser. Aqui está o resultado do código acima:

heapMemStats:used: 2.0 MB / 201 MB (0%) nativeMemStats:used: 3.6 MB / 6.3 MB (57%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 290 MB / 310 MB (93%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 553 MB / 579 MB (95%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 821 MB / 847 MB (96%)

Quando chego ao ponto em que o dispositivo não pode armazenar mais bitmaps (parado em 1,1 GB ou ~ 850 MB no meu Nexus 5x), em vez da exceção OutOfMemory, recebo ... nada! Apenas fecha o aplicativo. Sem sequer um diálogo dizendo que caiu.

Caso eu apenas crie um novo Bitmap, em vez de decodificá-lo (código disponível acima, apenas em um comentário), recebo um log estranho, dizendo que eu uso toneladas de GBs e toneladas de GBs de memória nativa disponíveis:

Além disso, ao contrário de quando decodico bitmaps, recebo uma falha aqui (incluindo uma caixa de diálogo), mas não é OOM. Em vez disso, é ... NPE!

01-04 10: 12: 36.936 30598-31301 / com.example.user.myapplication E / AndroidRuntime: FATAL EXCEPTION: AsyncTask # 1 Processo: com.example.user.myapplication, PID: 30598 java.lang.NullPointerException: tentativa de invoque o método virtual 'void android.graphics.Bitmap.setHasAlpha (boolean)' em uma referência de objeto nulo em android.graphics.Bitmap.createBitmap (Bitmap.java:1046) em android.graphics.Bitmap.createBitmap (Bitmap.java:980 ) em android.graphics.Bitmap.createBitmap (Bitmap.java:930) em android.graphics.Bitmap.createBitmap (Bitmap.java:891) em com.example.user.myapplication.MainActivity $ onCreate $ 1 $ 1.run (MainActivity. kt: 21) em android.os.AsyncTask $ SerialExecutor $ 1.run (AsyncTask.java:245) em java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1162) em java.util.concurrent.ThreadPoolExecutor $ Worker. execute (ThreadPoolExecutor.java:636) em java.lang.Thread.run (Thread.java:764)

Olhando para o gráfico do criador de perfil, fica ainda mais estranho. O uso da memória parece não aumentar muito e, no ponto de travamento, cai:

Se você olhar para o gráfico, verá muitos ícones do GC (a lixeira). Eu acho que pode estar fazendo alguma compressão de memória.

Se eu fizer um despejo de memória (usando o criador de perfil), em oposição às versões anteriores do Android, não consigo mais visualizar os Bitmaps.

As questões

Esse novo comportamento levanta muitas questões. Isso pode reduzir o número de falhas do OOM, mas também pode dificultar a detecção, o vazamento de memória e a correção. Talvez um pouco do que eu vi sejam apenas erros, mas ainda assim ...

O que exatamente mudou no uso de memória no Android O? E porque?

Como os Bitmaps são manipulados?

Ainda é possível visualizar Bitmaps nos relatórios de despejo de memória?

Qual é a maneira correta de obter a memória nativa máxima que o aplicativo pode usar, imprimi-la nos logs e usá-la como algo para decidir sobre o max?

Existe algum vídeo / artigo sobre este tópico? Eu não estou falando sobre otimizações de memória que foram adicionadas, mas mais sobre como os Bitmaps são alocados agora, como lidar com o OOM agora, etc ...

Eu acho que esse novo comportamento pode afetar algumas bibliotecas de cache, certo? Isso ocorre porque eles podem depender do tamanho da memória da pilha.

Como eu poderia criar tantos bitmaps, cada um com tamanho 20.000x20.000 (significando ~ 1,6 GB), mas quando eu só consegui criar alguns deles a partir da imagem real de tamanho 7.680x7.680 (significando ~ 236MB) ? Realmente compacta a memória, como já adivinhei?

Como as funções de memória nativa podem retornar valores tão grandes no caso da criação de bitmap, ainda mais razoavelmente para quando decodifiquei bitmaps? O que eles querem dizer?

O que há com o gráfico de perfil estranho no caso de criação de Bitmap? Quase não aumenta o uso da memória e, no entanto, chegou a um ponto em que não pode mais criar, eventualmente (depois de muitos itens serem inseridos).

O que há com o comportamento de exceções estranhas? Por que na decodificação de bitmap não recebi nenhuma exceção ou até mesmo um log de erros como parte do aplicativo e, quando os criei, recebi o NPE?

A Play Store detectará o OOM e ainda informará sobre eles, caso o aplicativo trave por causa disso? Será que vai detectá-lo em todos os casos? O Crashlytics pode detectá-lo? Existe uma maneira de ser informado sobre isso, seja pelos usuários ou durante o desenvolvimento no escritório?

questionAnswers(1)

yourAnswerToTheQuestion