Manter o trabalho em segundo plano ativo com um processo enxuto

Muitos apps realizam tarefas legítimas quando a IU não está mais visível, como fazer o download de grandes pacotes de recursos, sincronizar bancos de dados locais, transmitir áudio, registrar exercícios físicos ou manter uma sessão de dispositivo conectado. Quando esse trabalho em segundo plano é executado no mesmo processo do SO que a IU em primeiro plano e o mecanismo gráfico, todo o processo permanece em um nível de importância elevado (oom_score_adj entre 0 e 250), junto com toda a memória alocada pela IU.

Esta página explica por que isso prejudica seu app e o restante do dispositivo, como mover o trabalho em segundo plano para um processo leve e dedicado usando android:process e como verificar a economia de memória usando adb shell e o Perfetto.

Por que o trabalho de processo único retém a memória da interface

O Android gerencia a memória e o encerramento de processos na granularidade de um processo do SO (pid), não de componentes individuais (Activity, Service ou BroadcastReceiver). O Activity Manager (OomAdjuster) atribui a cada processo um único valor de oom_score_adj que reflete o componente ativo mais importante ou a vinculação de entrada nesse processo:

  • Quando o usuário interage com o Activity, o processo é executado no estado TOP (oom_score_adj = 0).
  • Quando o usuário sai do app e não há componentes ativos em segundo plano, o processo é movido para o bucket PREV (oom_score_adj = 700) ou CACHED (oom_score_adj >= 900). No bucket em cache, a plataforma congela o processo com o freezer do cgroup v2, compacta a memória com CachedAppOptimizer e permite que o lmkd encerre o processo primeiro sob pressão de memória.
  • No entanto, se o mesmo processo também executar um serviço em primeiro plano (como os tipos dataSync, mediaPlayback, connectedDevice ou location, em oom_score_adj = 200) ou hospedar um serviço ao qual o sistema ou outro cliente importante se vincula, todo o processo vai permanecer com essa prioridade elevada mesmo depois que o Activity for interrompido.
Comparação entre a colocalização de processo único que mantém a memória da interface fixada
       em oom_score_adj 200 e o isolamento de processo dividido que mantém apenas o
       worker em oom_score_adj 200 enquanto o processo da interface é descartado para o cache.
Figura 1. Co-localização de processo único em comparação com isolamento de processo dividido durante o trabalho em segundo plano.

Quando um processo de interface pesado permanece em oom_score_adj = 200, várias categorias grandes de memória permanecem residentes:

  • Activity interrompida e hierarquia de visualização: a menos que a Activity seja explicitamente concluída, a Window, a árvore de visualização, as estruturas de layout e o estado de visualização permanecem acessíveis no heap do Java enquanto o usuário está em outro app.
  • Contextos gráficos nativos e alocações de GPU: o HardwareRenderer padrão mantém o contexto e os caches da GPU, enquanto um Activity interrompido reside em um processo que não é armazenado em cache. Os apps e mecanismos de jogo que criam os próprios contextos do Vulkan (VkDevice, VkSwapchainKHR, pools de descritores, caches de pipeline) ou do OpenGL ES (EGLContext, texturas, buffers de vértice) também mantêm as estruturas de driver, os programas de sombreador e os buffers gráficos, a menos que o mecanismo desative explicitamente o dispositivo gráfico quando interrompido.
  • Fragmentação de heap e sobrecarga de ambiente de execução: mesmo quando um app libera caches em onTrimMemory, páginas de alocador nativo parcialmente preenchidas, páginas de cópia em gravação Zygote sujas, bibliotecas compartilhadas carregadas (.so) e metadados de ambiente de execução permanecem residentes na memória privada do processo. Como o processo está acima do limite de armazenamento em cache (oom_score_adj < 900), a plataforma não o congela nem executa a compactação de apps armazenados em cache. Para conferir mais de perto esse efeito com serviços vinculados, consulte Como colocar um serviço vinculado com uma interface que consome muita memória.

Como um processo em segundo plano otimizado protege seu app

Manter a memória do tamanho do primeiro plano em um estado de segundo plano coloca seu próprio trabalho em segundo plano em risco:

  • Os limites de memória em segundo plano são mais rígidos do que os limites em primeiro plano. No Android 17 e versões mais recentes, o Limitador de memória aplica um limite de memória não visível menor ao app quando nenhum dos processos mostra uma interface visível, inclusive durante a execução de um serviço em primeiro plano. O limite abrange todos os processos do app juntos. Se o processo que executa o serviço em primeiro plano ainda mantiver a memória de interface e a memória gráfica, essa memória será contabilizada no limite menor. Quando o app excede o limite, o kernel o limita enquanto recupera as páginas, o que desacelera o download, a sincronização ou a reprodução em execução. Se o app continuar alocando depois que a troca for esgotada, o sistema vai encerrá-lo. Para verificar se o app foi afetado, consulte Limites de memória do app.
  • Os processos maiores são encerrados primeiro quando há pressão de memória. Quando o lmkd precisa liberar memória e está configurado para escolher o processo mais pesado em um bucket de importância (ro.lmk.kill_heaviest_task), um processo de serviço em primeiro plano que também contém centenas de MB de memória de interface é encerrado antes de processos menores com a mesma importância, e o trabalho em segundo plano é interrompido com ele. Consulte Interação entre kswapd e lmkd.
  • O estado da interface em cache pode ser congelado, compactado e gravado de volta na memória flash. Permitir que o estado da interface em primeiro plano seja movido para um processo em cache permite que o SO congele o processo, compacte a memória na zRAM e grave as páginas frias de volta no armazenamento flash. O consumo de memória residente diminui enquanto o estado da interface permanece intacto, dando ao trabalho em segundo plano e à interface em cache a melhor chance de sobreviver até que o usuário retorne.
  • Os orçamentos de memória por processo se tornam práticos. Um processo que executa apenas trabalho em segundo plano tem um conjunto de trabalho pequeno e previsível. Portanto, é possível fornecer um orçamento de memória restrito sem limitar a interface.

A execução de um segundo processo pode parecer que aumenta o consumo total de memória do app, porque um segundo ambiente de execução tem um pequeno custo de referência. Essa comparação só é válida se um único processo puder liberar completamente todo o estado da IU e dos gráficos sempre que fizer a transição para o trabalho em segundo plano, o que os apps e os kits de ferramentas de IU não podem fazer de forma confiável na prática. Permitir que o SO congele, compacte e grave de volta um processo de interface em cache separado usa muito menos memória física no caso comum, mantém o app dentro do limite não visível e reduz as finalizações por falta de memória para o app e em todo o dispositivo.

Isolar o trabalho em segundo plano em um processo dedicado

Para eliminar a residência de IU e gráficos durante o trabalho em segundo plano, execute componentes de segundo plano de longa duração em um processo leve dedicado, mantendo a IU e o mecanismo de gráficos no processo principal:

Processo Declaração do manifesto Componentes hospedados Estado quando o usuário sai da interface durante o trabalho em segundo plano
Processo principal da interface Padrão (com.example.app) Activity, visualizações de interface, caches de imagem, renderizador Vulkan ou OpenGL ES, mecanismo de áudio e cena Cai para PREV e depois para CACHED (oom_score_adj >= 900). Congelado, compactado e gravado de volta no flash.
Processo de worker em segundo plano android:process=":bg" Service em primeiro plano, downloader de recursos em segundo plano, worker de sincronização ou gerenciador de dispositivos conectados É executado em FGS (oom_score_adj = 200) apenas com o heap e o código do worker, sem visualizações de interface ou contextos de GPU.

Declarar os orçamentos de processo e memória

Atribua o plano de fundo Service a um subprocesso particular definindo android:process=":bg" em AndroidManifest.xml. Os dois-pontos iniciais (:) criam um processo particular com escopo para o UID do app.

No Android 17 QPR2 (nível 37.2 da API) e versões mais recentes, combine a divisão de processos com orçamentos de memória do app por processo dentro do bloco <processes> para que o sistema operacional aplique um limite rígido de conjunto de trabalho ao worker em segundo plano sem restringir o processo de interface em primeiro plano:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".HeavyApplication"
        android:label="@string/app_name">

        <!-- App-wide budgets that cover every process, including :bg -->
        <memory-budget android:maxMb="512" />
        <memory-budget
            android:maxMb="128"
            android:state="background" />

        <processes>
            <!-- Tight budget for the headless background worker process -->
            <process android:process=":bg">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <activity
            android:name=".MainActivity"
            android:exported="true" />

        <service
            android:name=".AssetDownloadService"
            android:process=":bg"
            android:foregroundServiceType="dataSync"
            android:exported="false" />

    </application>
</manifest>

O uso de memória em :bg é contabilizado no próprio orçamento e no orçamento de todo o app. Em dispositivos com versões anteriores ao Android 17 QPR2, o sistema ignora <memory-budget>, enquanto android:process=":bg" funciona em todos os níveis da API.

Inicialização de portão em Application.onCreate

O Android instancia sua classe Application e chama Application.onCreate em todos os processos gerados para seu pacote. Uma armadilha comum ao adotar o android:process=":bg" é deixar kits de ferramentas de interface pesados, pools de carregamento de imagens, SDKs do Analytics ou inicialização de mecanismo nativo (System.loadLibrary) dentro de um método Application.onCreate desprotegido. Isso infla o processo :bg com a mesma pegada de referência que você está tentando evitar.

Verifique Application.getProcessName, que está disponível no Android 9 (nível 28 da API) e versões mais recentes, em onCreate e inicialize apenas as dependências exigidas pelo processo atual:

Kotlin

class HeavyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        val processName = Application.getProcessName()
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess()
            return
        }
        initMainUiProcess()
    }

    private fun initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private fun initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni")
    }

    companion object {
        private const val BG_PROCESS_SUFFIX = ":bg"
    }
}

Java

public final class HeavyApplication extends Application {
    private static final String BG_PROCESS_SUFFIX = ":bg";

    @Override
    public void onCreate() {
        super.onCreate();
        String processName = Application.getProcessName();
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess();
            return;
        }
        initMainUiProcess();
    }

    private void initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private void initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni");
    }
}

As bibliotecas que são inicializadas por um provedor de conteúdo, como a App Startup do Jetpack, são inicializadas apenas no processo principal, porque um provedor de conteúdo declarado sem android:process é executado no processo principal. Se o código em :bg depender de uma biblioteca desse tipo, inicialize-a explicitamente em initBackgroundWorkerProcess.

Se você criar com um mecanismo de jogo, como o Unity, implemente o serviço em segundo plano em uma biblioteca ou um plug-in simples do Android escrito em Java ou Kotlin que não faça referência a classes de mecanismo, como UnityPlayer. O processo :bg nunca carrega as bibliotecas nativas do mecanismo. Para confirmar, verifique se adb shell cat /proc/<pid>/maps para o processo :bg não lista libunity.so ou libil2cpp.so.

Comunicar sem fixar o processo da interface

Quando o usuário tem MainActivity aberto enquanto AssetDownloadService é executado em :bg, a interface geralmente precisa mostrar o progresso do download em tempo real. A importância do processo flui de um cliente para o processo que hospeda o serviço ou provedor de conteúdo que ele usa. Portanto, a direção de cada dependência é importante:

  • A vinculação da IU ao worker é segura. Quando MainActivity se vincula a AssetDownloadService, o sistema eleva :bg à importância da IU visível. Quando o usuário sai, o processo da interface é armazenado em cache, e a vinculação não eleva mais nada.
  • O worker que depende do processo de interface não é seguro. Se o código em :bg se vincular a um serviço ou consultar um provedor de conteúdo que é executado no processo principal, o sistema vai elevar o processo principal à importância elevada do worker, e a memória da interface vai ficar fixada novamente. Em vez disso, faça com que a interface registre um callback com o worker, como um Messenger ou uma interface de callback da AIDL. Transmitir um objeto binder como um callback não muda a importância de nenhum dos processos.

Cancele o registro do callback antes de desvincular para que o worker nunca chame o processo da interface depois que o sistema o congelar. O exemplo a seguir se vincula ao worker apenas enquanto a IU está visível:

Kotlin

class MainActivity : Activity() {
    private var downloadService: IBinder? = null

    private val downloadConnection = object : ServiceConnection {
        override fun onServiceConnected(
                name: ComponentName,
                service: IBinder
        ) {
            downloadService = service
            // Register a Messenger or AIDL callback for progress updates.
        }

        override fun onServiceDisconnected(name: ComponentName) {
            // The binding stays active and reconnects if the worker restarts.
            downloadService = null
        }
    }

    override fun onStart() {
        super.onStart()
        val intent = Intent(this, AssetDownloadService::class.java)
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection)
    }
}

Java

public final class MainActivity extends Activity {
    private IBinder downloadService;

    private final ServiceConnection downloadConnection =
            new ServiceConnection() {
                @Override
                public void onServiceConnected(
                        ComponentName name, IBinder service) {
                    downloadService = service;
                    // Register a Messenger or AIDL callback for progress
                    // updates.
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                    // The binding stays active and reconnects if the worker
                    // restarts.
                    downloadService = null;
                }
            };

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, AssetDownloadService.class);
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null;
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection);
    }
}

Para saber mais sobre como as flags de vinculação afetam o oom_score_adj em todos os processos, consulte Vinculações de serviço e estados de processo.

Coordenar o estado compartilhado no disco com segurança

Como o processo principal e o :bg são executados em espaços de endereço separados, com ambientes de execução e descritores de arquivo separados:

  • SQLite e Room: use o registro prévio de escrita (o modo de diário padrão no Room na maioria dos dispositivos) para que os dois processos possam ler e gravar no banco de dados simultaneamente. Não chame enableMultiInstanceInvalidation de :bg. O Room hospeda o serviço de invalidação no processo principal. Assim, o worker se vincula ao processo principal e mantém a memória da interface com a importância elevada do worker. Em vez disso, faça com que o worker informe as mudanças pelo retorno de chamada que a interface registra e faça com que a interface consulte novamente o banco de dados quando ele for iniciado.
  • Preferências: não compartilhe SharedPreferences entre processos. Use o DataStore em códigos com vários processos ou transmita o estado explicitamente pela IPC.
  • Arquivos de recursos: grave os pacotes de recursos baixados em um arquivo de preparo em :bg, chame fdatasync e, em seguida, posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) em :bg após a gravação (conforme mostrado em Retornar páginas de memória para o SO) e renomeie o arquivo de forma atômica. Deixe que a UI colete o resultado na próxima vez que for iniciada e se vincule ao worker, em vez de chamar um processo de UI que pode estar congelado.

Observar o efeito no adb shell

Para verificar se a divisão dos processos em primeiro e segundo plano isola o uso elevado de memória, inspecione os estados do processo do Activity Manager e dumpsys meminfo depois de acessar a tela inicial. A saída de exemplo nesta seção vem do app de exemplo MemoryLab, que executa a mesma alocação nativa de 64 MB no processo de interface em ambas as configurações e hospeda o serviço em primeiro plano dividido em um processo :remote.

1. Comparar estados de processo e pontuações de OOM

Inicie a carga de trabalho em segundo plano, pressione o botão home para que o Activity seja interrompido e consulte o Activity Manager:

adb shell dumpsys activity processes com.example.app | grep -E "Proc #|oom:|state:"

Em uma arquitetura de processo único, o processo principal permanece elevado, mesmo que o Activity seja interrompido. Por cerca de 15 segundos após o usuário sair, o Activity Manager mantém um processo que executa um serviço em primeiro plano em oom_score_adj = 50 (fg-service-act) e o reduz para 200 (fg-service). A saída abaixo foi capturada 20 segundos após o usuário pressionar o botão home:

    Proc #1: prcp     F/S/FGS  ---NFUATI  t: 0 13794:com.android.memorylab/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00

Em uma arquitetura de processo dividido, apenas o processo de worker permanece em oom_score_adj = 200, enquanto o processo principal da interface cai para PREV (oom_score_adj = 700) e, depois, para o bucket em cache (oom_score_adj >= 900):

    Proc #3: prcp     F/S/FGS  ---NFUATI  t: 0 13544:com.android.memorylab:remote/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00
    Proc #1: prev     b/ /LAST -------TI  t: 0 13312:com.android.memorylab/u0a284 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00

Depois que o tempo limite do app anterior expira, o processo principal é movido para o bucket em cache (cch, oom_score_adj = 900, previous-expired) e perde todos os recursos de processo (---------), incluindo o tempo de CPU implícito (I). Nesse momento, o sistema o congela e pode compactá-lo ou recuperá-lo a qualquer momento sem interromper o processo de worker.

2. Comparar dumpsys meminfo entre processos

Compare o resumo do dumpsys meminfo do worker elevado com o processo da interface:

adb shell dumpsys meminfo com.example.app:bg
adb shell dumpsys meminfo com.example.app

Exemplo de saída App Summary para o processo de worker do MemoryLab em oom_score_adj = 200:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:      380                          13840
         Native Heap:     1548                           2832
                Code:      132                          55816
               Stack:      200                            208
            Graphics:        0                              0
       Private Other:      496
              System:     1926
             Unknown:                                    4168

           TOTAL PSS:     4682            TOTAL RSS:    76864       TOTAL SWAP PSS:      441

Exemplo de saída de App Summary para o processo de IU do MemoryLab em oom_score_adj = 700, que contém a alocação nativa de 64 MB em Private Other:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:     1316                          19816
         Native Heap:    30408                          31596
                Code:     3148                         115796
               Stack:      656                            664
            Graphics:        0                              0
       Private Other:    67044
              System:    13687
             Unknown:                                   71092

           TOTAL PSS:   116259            TOTAL RSS:   238964       TOTAL SWAP PSS:      259

O processo de worker precisa de menos de 5 MB de PSS, porque compartilha a maior parte do código e das páginas de execução com o Zygote. Em um app que renderiza com Vulkan ou OpenGL ES, a linha Graphics do processo de interface também inclui a memória da GPU. Este exemplo foi capturado em um dispositivo virtual Cuttlefish com renderização de software, então Graphics é 0 em ambos os processos.

3. Inspecionar a contabilização do cgroup por processo

Cada processo no seu UID tem o próprio diretório cgroup v2 dentro de um cgroup pai para o app. O comando a seguir lista oom_score_adj, memory.current e o memory.high de todo o app para cada processo do app:

adb shell '
  for PID in $(pidof com.example.app com.example.app:bg); do
    CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
    echo "pid=$PID ($(tr -d "\0" < /proc/$PID/cmdline))" \
      "adj=$(cat /proc/$PID/oom_score_adj)" \
      "memory.current=$(cat $CG/memory.current)" \
      "app.memory.high=$(cat ${CG%/*}/memory.high)"
  done
'

Exemplo de saída do MemoryLab depois de pressionar "Início":

pid=15301 (com.android.memorylab) adj=700 memory.current=103260160 app.memory.high=966787072
pid=16518 (com.android.memorylab:remote) adj=200 memory.current=3608576 app.memory.high=966787072

O processo de worker é cobrado em menos de 4 MB, enquanto o processo de interface é cobrado em cerca de 98 MB. Cada processo tem o próprio memory.current, mas o limitador de memória define o limite no cgroup pai que contém todos os processos do app. Ambas as linhas mostram o mesmo app.memory.high de 966.787.072 bytes, que é o limite não visível de 922 MB na saída a seguir.

Para ver os limites visíveis e não visíveis que o Limitador de Memória aplica ao dispositivo, execute o seguinte comando:

adb shell am memory-limiter status

Exemplo de saída de um dispositivo com 4 GB de RAM, em que o limite não visível é metade do limite visível:

Memory limiter
  enabled                  limits=true              killing=true             ignored=none
  visible:                 memHigh=1843MB           memMax=2048MB            swapMax=1024MB
  not-visible:             memHigh=922MB            memMax=1024MB            swapMax=1024MB

Observar o efeito no Perfetto

O Perfetto permite visualizar a linha do tempo de oom_score_adj e RSS nos dois processos enquanto o usuário sai da IU em primeiro plano para a tela inicial, enquanto o trabalho em segundo plano continua.

1. Gravar um rastro

Registre um rastro de 15 segundos que capture contadores de memória de processo, memória de GPU e mudanças de oom_score_adj ao iniciar um job em segundo plano no app e navegar até a tela inicial:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/process-splitting.perfetto-trace <<'EOF'
buffers: {
  size_kb: 32768
  fill_policy: DISCARD
}
data_sources: {
  config {
    name: "linux.process_stats"
    process_stats_config {
      scan_all_processes_on_start: true
      proc_stats_poll_ms: 250
    }
  }
}
data_sources: {
  config {
    name: "android.gpu.memory"
  }
}
data_sources: {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "gpu_mem/gpu_mem_total"
    }
  }
}
duration_ms: 15000
EOF
adb pull /data/misc/perfetto-traces/process-splitting.perfetto-trace

2. Inspecionar faixas de processo na interface do Perfetto

Abra o rastro na interface do Perfetto e expanda os grupos de processos para com.example.app e com.example.app:bg:

  1. Fixe as faixas oom_score_adj e mem.rss.anon para os dois processos para que elas apareçam lado a lado na parte de cima da linha do tempo.
  2. Localize o carimbo de data/hora em que o usuário pressiona "Início":
    • No processo de worker, oom_score_adj permanece em 200 e mem.rss.anon permanece estável e pequeno.
    • No processo da interface, o oom_score_adj sobe de 0 para 200, permanece lá por cerca de um segundo enquanto o Activity é interrompido e, em seguida, sobe para 700 (PREV). Após o tempo limite do app anterior, ele é movido para o intervalo em cache, em que o sistema pode compactar e recuperar a memória sem afetar o worker.
Rastros do Perfetto para o processo de interface e o processo de worker. O oom_score_adj
       do processo de interface aumenta de 0 para 200 e depois para 700 após a tela inicial, enquanto
       o processo de worker permanece em 200 com cerca de 19 MB de RSS anônimo.
Figura 2. Divida os processos depois que o usuário pressionar "Início".

Na Figura 2, o processo de IU permanece brevemente em 200 enquanto o Activity é interrompido e, em seguida, sobe para 700, enquanto o worker permanece em 200 com cerca de 19 MB de RSS anônimo. Para comparação, a Figura 3 mostra um rastro de 35 segundos da configuração de colocalização, em que o processo único permanece em 50 e, em seguida, em 200 com toda a memória da interface.

As faixas do Perfetto para um único processo colocalizado. oom_score_adj
       aumenta de 0 para 50 após a tela inicial e para 200 cerca de 15 segundos depois, enquanto
       o RSS anônimo permanece alto.
Figura 3. Um processo colocalizado depois que o usuário pressiona "Início".

Para conferir essas visualizações, abra o trace dividido e o trace colocalizado na interface do Perfetto.

3. Consultar o consumo de memória elevado com o PerfettoSQL

Execute a seguinte consulta PerfettoSQL na guia Query (SQL) para medir o RSS anônimo e de arquivo médio mantido por cada processo do app em cada nível oom_score_adj:

INCLUDE PERFETTO MODULE android.memory.process;

SELECT
  process_name,
  score AS oom_score_adj,
  ROUND(SUM(dur) / 1e9, 2) AS duration_sec,
  ROUND(SUM(anon_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_anon_mb,
  ROUND(SUM(file_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_file_mb
FROM memory_oom_score_with_rss_and_swap_per_process
WHERE process_name GLOB 'com.example.app*'
GROUP BY process_name, score
ORDER BY process_name, score;

Exemplo de saída para o trace dividido na Figura 2:

process_name                  oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab                     0          4.11         93.3        110.4
com.android.memorylab                   200          1.06        126.8        110.5
com.android.memorylab                   700          9.72        115.1        110.5
com.android.memorylab:remote              0          0.01         NULL         NULL
com.android.memorylab:remote            200         12.87         18.9         53.7

Exemplo de saída para o trace colocalizado na Figura 3:

process_name           oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab              0          4.10         96.0        110.4
com.android.memorylab             50         16.04        117.6        110.5
com.android.memorylab            200         14.68        116.5        110.6

Na configuração dividida, apenas cerca de 19 MB de memória anônima permanecem em oom_score_adj = 200 após o primeiro segundo. Na configuração de colocalização, cerca de 117 MB permanecem em 50 e, em seguida, em 200 enquanto o serviço em primeiro plano estiver em execução.


← Retornar páginas de memória para o SO | ↑ Para cima