Verificações dexpreopt e <uses-library>

O Android 12 apresenta mudanças no sistema de build para a compilação AOT de arquivos DEX (dexpreopt) em módulos Java que têm dependências <uses-library>. Em alguns casos, essas mudanças no sistema de build podem corromper os builds. Use esta página para se preparar para falhas e siga as instruções nesta página para corrigi-las e mitigá-las.

O dexpreopt é o processo de compilação antecipada de bibliotecas e apps Java. O Dexpreopt ocorre no host no momento da criação, ao contrário do dexopt, que acontece no dispositivo. A estrutura de dependências de biblioteca compartilhada usada por um módulo Java (uma biblioteca ou um app) é conhecida como contexto do carregador de classe (CLC, na sigla em inglês). Para garantir a correção do dexpreopt, os CLCs de build-time e run-time precisam coincidir. O CLC no build é o que o compilador dex2oat usa no momento dexpreopt (ele é registrado nos arquivos ODEX), e o CLC no tempo de execução é o contexto em que o código pré-compilado é carregado no dispositivo.

Essas CLCs de tempo de build e de execução precisam coincidir por motivos de precisão e desempenho. Para correção, é necessário lidar com classes duplicadas. Se as dependências da biblioteca compartilhada no momento da execução forem diferentes das usadas para compilação, algumas das classes poderão ser resolvidas de forma diferente, causando bugs sutis durante a execução. O desempenho também é afetado pelas verificações de tempo de execução para classes duplicadas.

Casos de uso afetados

A primeira inicialização é o principal caso de uso afetado por essas mudanças: se o ART detectar uma incompatibilidade entre CLCs de tempo de build e de execução, ele rejeitará os artefatos de dexpreopt e executará o dexopt. Para inicializações seguintes, isso não é um problema, porque os apps podem ser dexoptados em segundo plano e armazenados no disco.

Áreas afetadas do Android

Isso afeta todos os apps e bibliotecas Java que têm dependências de ambiente de execução em outras bibliotecas Java. O Android tem milhares de apps, e centenas deles usam bibliotecas compartilhadas. Os parceiros também são afetados, já que têm as próprias bibliotecas e apps.

Interromper mudanças

O sistema de build precisa conhecer as dependências <uses-library> antes de gerar regras de build de dexpreopt. No entanto, ele não pode acessar o manifesto diretamente e ler as tags <uses-library> nele, porque o sistema de build não tem permissão para ler arquivos arbitrários ao gerar regras de build (por motivos de desempenho). Além disso, o manifesto pode ser empacotado em um APK ou um pré-build. Portanto, as informações de <uses-library> precisam estar presentes nos arquivos de build (Android.bp ou Android.mk).

Anteriormente, o ART usava uma solução alternativa que ignorava as dependências da biblioteca compartilhada (conhecida como &-classpath). Isso não era seguro e causava bugs sutis, por isso a solução alternativa foi removida no Android 12.

Como resultado, os módulos Java que não fornecem informações de <uses-library> corretas nos arquivos de build podem causar falhas de build (causadas por uma incompatibilidade de CLC no momento da criação) ou regressões no tempo de primeira inicialização (causadas por uma incompatibilidade de CLC no momento da inicialização seguida por dexopt).

Caminho de migração

Siga estas etapas para corrigir um build corrompido:

  1. Para desativar globalmente a verificação do tempo de build de um produto específico, defina

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    no makefile do produto. Isso corrige erros de build, exceto para casos especiais, listados na seção Como corrigir falhas. No entanto, essa é uma solução temporária que pode causar incompatibilidade de CLC no momento da inicialização seguida de dexopt.

  2. Corrija os módulos que falharam antes de você desativar globalmente a verificação de tempo de build adicionando as informações <uses-library> necessárias aos arquivos de build. Consulte Como corrigir falhas para mais detalhes. Para a maioria dos módulos, isso requer a adição de algumas linhas em Android.bp ou Android.mk.

  3. Desative a verificação de tempo de build e a descontinuação para os casos problemáticos por módulo. Desative o dexpreopt para não perder tempo de build e armazenamento em artefatos que são rejeitados na inicialização.

  4. Reative a verificação de tempo de build globalmente ao desmarcar PRODUCT_BROKEN_VERIFY_USES_LIBRARIES que foi definido na etapa 1. O build não vai falhar após essa mudança (por causa das etapas 2 e 3).

  5. Corrija os módulos que você desativou na Etapa 3, um de cada vez, e reative o dexpreopt e a verificação <uses-library>. Registre bugs, se necessário.

As verificações de <uses-library> no momento da criação são aplicadas no Android 12.

Corrigir falhas

As seções a seguir mostram como corrigir tipos específicos de falha.

Erro de build: incompatibilidade de CLC

O sistema de build faz uma verificação de coerência no tempo de build entre as informações nos arquivos Android.bp ou Android.mk e o manifesto. O sistema de build não pode ler o manifesto, mas pode gerar regras de build para ler o manifesto (extraindo-o de um APK, se necessário) e comparar tags <uses-library> no manifesto com as informações de <uses-library> nos arquivos de build. Se a verificação falhar, o erro será parecido com este:

error: mismatch in the <uses-library> tags between the build system and the manifest:
    - required libraries in build system: []
                     vs. in the manifest: [org.apache.http.legacy]
    - optional libraries in build system: []
                     vs. in the manifest: [com.x.y.z]
    - tags in the manifest (.../X_intermediates/manifest/AndroidManifest.xml):
        <uses-library android:name="com.x.y.z"/>
        <uses-library android:name="org.apache.http.legacy"/>

note: the following options are available:
    - to temporarily disable the check on command line, rebuild with RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)
    - to temporarily disable the check for the whole product, set PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles
    - to fix the check, make build system properties coherent with the manifest
    - see build/make/Changes.md for details

Como a mensagem de erro sugere, há várias soluções, dependendo da urgência:

  • Para uma correção temporária em todo o produto, defina PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true no makefile do produto. A verificação de coerência do tempo de build ainda é executada, mas uma falha de verificação não significa uma falha no build. Em vez disso, uma falha de verificação faz com que o sistema de build faça o downgrade do filtro do compilador dex2oat para verify no dexpreopt, o que desativa totalmente a compilação AOT para esse módulo.
  • Para uma correção rápida e global de linha de comando, use a variável de ambiente RELAX_USES_LIBRARY_CHECK=true. Ele tem o mesmo efeito que PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, mas é destinado ao uso na linha de comando. A variável de ambiente substitui a variável do produto.
  • Para uma solução de correção da causa raiz do erro, informe ao sistema de build as tags <uses-library> no manifesto. Uma inspeção da mensagem de erro mostra quais bibliotecas causam o problema, assim como a inspeção AndroidManifest.xml ou o manifesto dentro de um APK que pode ser verificado com `aapt dump badging $APK | grep uses-library`.

Para módulos Android.bp:

  1. Procure a biblioteca ausente na propriedade libs do módulo. Se estiver presente, o Soong normalmente vai adicionar essas bibliotecas automaticamente, exceto nestes casos especiais:

    • Ela não é uma biblioteca do SDK. Ela é definida como java_library em vez de java_sdk_library.
    • A biblioteca tem um nome de biblioteca diferente (no manifesto) do nome do módulo (no sistema de build).

    Para corrigir isso temporariamente, adicione provides_uses_lib: "<library-name>" na definição da biblioteca Android.bp. Para uma solução de longo prazo, corrija o problema subjacente: converta a biblioteca em uma biblioteca do SDK ou renomeie o módulo.

  2. Se a etapa anterior não fornecer uma resolução, adicione uses_libs: ["<library-module-name>"] para bibliotecas obrigatórias ou optional_uses_libs: ["<library-module-name>"] para bibliotecas opcionais à definição Android.bp do módulo. Essas propriedades aceitam uma lista de nomes de módulos. A ordem relativa das bibliotecas na lista precisa ser a mesma que a ordem no manifesto.

Para módulos Android.mk:

  1. Confira se a biblioteca tem um nome de biblioteca diferente (no manifesto) do nome do módulo (no sistema de build). Se for o caso, corrija isso temporariamente adicionando LOCAL_PROVIDES_USES_LIBRARY := <library-name> ao arquivo Android.mk da biblioteca ou adicione provides_uses_lib: "<library-name>" ao arquivo Android.bp da biblioteca. Os dois casos são possíveis, já que um módulo Android.mk pode depender de uma biblioteca Android.bp. Para uma solução de longo prazo, corrija o problema: renomeie o módulo da biblioteca.

  2. Adicione LOCAL_USES_LIBRARIES := <library-module-name> para bibliotecas necessárias. Adicione LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> para bibliotecas opcionais à definição Android.mk do módulo. Essas propriedades aceitam uma lista de nomes de módulos. A ordem relativa das bibliotecas na lista precisa ser a mesma do manifesto.

Erro de build: caminho da biblioteca desconhecido

Se o sistema de build não encontrar um caminho para um jar DEX <uses-library> (um caminho de build no host ou um caminho de instalação no dispositivo), o build geralmente falha. Uma falha ao encontrar um caminho pode indicar que a biblioteca está configurada de alguma maneira inesperada. Corrija o build temporariamente desativando o dexpreopt para o módulo com problemas.

Android.bp (propriedades do módulo):

enforce_uses_libs: false,
dex_preopt: {
    enabled: false,
},

Android.mk (variáveis de módulo):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Registre um bug para investigar cenários sem suporte.

Erro de build: dependência da biblioteca ausente

Uma tentativa de adicionar <uses-library> X do manifesto do módulo Y ao arquivo de build para Y pode resultar em um erro de build devido à ausência da dependência X.

Este é um exemplo de mensagem de erro para módulos Android.bp:

"Y" depends on undefined module "X"

Este é um exemplo de mensagem de erro para módulos Android.mk:

'.../JAVA_LIBRARIES/com.android.X_intermediates/dexpreopt.config', needed by '.../APPS/Y_intermediates/enforce_uses_libraries.status', missing and no known rule to make it

Uma fonte comum desses erros é quando uma biblioteca é nomeada de maneira diferente do módulo correspondente no sistema de build. Por exemplo, se a entrada <uses-library> do manifesto for com.android.X, mas o nome do módulo da biblioteca for apenas X, isso vai causar um erro. Para resolver esse caso, informe ao sistema de build que o módulo chamado X fornece um <uses-library> chamado com.android.X.

Este é um exemplo para bibliotecas Android.bp (propriedade do módulo):

provides_uses_lib: “com.android.X”,

Este é um exemplo para bibliotecas Android.mk (variável do módulo):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Incompatibilidade de CLC no tempo de inicialização

Na primeira inicialização, pesquise no logcat mensagens relacionadas à incompatibilidade do CLC, conforme mostrado abaixo:

$ adb wait-for-device && adb logcat \
  | grep -E 'ClassLoaderContext [a-z ]+ mismatch' -A1

A saída pode ter mensagens do formulário mostrado aqui:

[...] W system_server: ClassLoaderContext shared library size mismatch Expected=..., found=... (PCL[]... | PCL[]...)
[...] I PackageDexOptimizer: Running dexopt (dexoptNeeded=1) on: ...

Se você receber um aviso de incompatibilidade de CLC, procure um comando dexopt para o módulo com defeito. Para corrigir isso, verifique se a verificação no tempo de build do módulo é bem-sucedida. Se isso não funcionar, talvez o seu seja um caso especial que não tem suporte do sistema de build (como um app que carrega outro APK, não uma biblioteca). O sistema de build não processa todos os casos porque é impossível saber com certeza o que o app carrega durante a execução.

Contexto do carregador de classes

A CLC é uma estrutura semelhante a uma árvore que descreve a hierarquia do carregador de classes. O sistema de build usa o CLC em um sentido restrito (ele abrange apenas bibliotecas, não APKs ou carregadores de classe personalizados): é uma árvore de bibliotecas que representa o fechamento transitivo de todas as dependências <uses-library> de uma biblioteca ou app. Os elementos de nível superior de um CLC são as dependências <uses-library> diretas especificadas no manifesto (o caminho de classe). Cada nó de uma árvore de CLC é um nó <uses-library> que pode ter os próprios subnós <uses-library>.

Como as dependências <uses-library> são um gráfico acíclico direcionado e não necessariamente uma árvore, a CLC pode conter várias subárvores para a mesma biblioteca. Em outras palavras, a CLC é o gráfico de dependência "desdobrado" em uma árvore. A duplicação ocorre apenas em um nível lógico. Os carregadores de classe subjacentes reais não são duplicados (durante a execução, há uma única instância de carregador de classe para cada biblioteca).

A CLC define a ordem de pesquisa das bibliotecas ao resolver classes Java usadas pela biblioteca ou app. Essa ordem é importante porque as bibliotecas podem conter classes duplicadas e a classe é resolvida para a primeira correspondência.

CLC no dispositivo (tempo de execução)

PackageManager (em frameworks/base) cria uma CLC para carregar um módulo Java no dispositivo. Ele adiciona as bibliotecas listadas nas tags <uses-library> do manifesto do módulo como elementos CLC de nível superior.

Para cada biblioteca usada, a PackageManager recebe todas as dependências <uses-library> (especificadas como tags no manifesto dessa biblioteca) e adiciona um CLC aninhado para cada dependência. Esse processo continua recursivamente até que todos os nós de folha da árvore CLC construída sejam bibliotecas sem dependências de <uses-library>.

PackageManager só reconhece bibliotecas compartilhadas. A definição de "compartilhamento" é diferente do significado comum (como compartilhado versus estático). No Android, as bibliotecas compartilhadas Java são aquelas listadas em configurações XML instaladas no dispositivo (/system/etc/permissions/platform.xml). Cada entrada contém o nome de uma biblioteca compartilhada, um caminho para o arquivo jar DEX e uma lista de dependências (outras bibliotecas compartilhadas que essa usa no tempo de execução e especifica em tags <uses-library> no manifesto).

Em outras palavras, há duas fontes de informações que permitem que PackageManager construa o CLC no momento da execução: tags <uses-library> no manifesto e dependências de biblioteca compartilhada em configurações XML.

CLC no host (no build)

O CLC não é necessário apenas ao carregar uma biblioteca ou um app, mas também ao compilar um. A compilação pode acontecer no dispositivo (dexopt) ou durante o build (dexpreopt). Como o dexopt ocorre no dispositivo, ele tem as mesmas informações que PackageManager (manifestos e dependências de biblioteca compartilhada). No entanto, o Dexpreopt ocorre no host e em um ambiente totalmente diferente e precisa receber as mesmas informações do sistema de build.

Portanto, o CLC no tempo de build usado por dexpreopt e o CLC no tempo de execução usado por PackageManager são a mesma coisa, mas calculados de duas maneiras diferentes.

As CLCs de ambiente de build e de execução precisam coincidir. Caso contrário, o código compilado AOT criado pelo dexpreopt vai ser rejeitado. Para verificar a igualdade de CLCs de tempo de build e de execução, o compilador dex2oat registra CLCs de tempo de build nos arquivos *.odex (no campo classpath do cabeçalho do arquivo OAT). Para encontrar o CLC armazenado, use este comando:

oatdump --oat-file=<FILE> | grep '^classpath = '

A incompatibilidade de CLC no momento da criação e do tempo de execução é relatada no logcat durante a inicialização. Pesquise com este comando:

logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'

A incompatibilidade é ruim para o desempenho, porque força a biblioteca ou o app a ser dexoptado ou a ser executado sem otimizações. Por exemplo, o código do app pode precisar ser extraído na memória do APK, uma operação muito cara.

Uma biblioteca compartilhada pode ser opcional ou obrigatória. Do ponto de vista do dexpreopt, uma biblioteca necessária precisa estar presente no tempo de build (a ausência dela é um erro de build). Uma biblioteca opcional pode estar presente ou ausente no momento do build: se presente, ela é adicionada ao CLC, transmitida para o dex2oat e gravada no arquivo *.odex. Se não houver uma biblioteca opcional, ela será ignorada e não será adicionada à CLC. Se houver uma incompatibilidade entre o status de build e de execução (a biblioteca opcional está presente em um caso, mas não no outro), os CLCs de build e de execução não vão corresponder, e o código compilado será rejeitado.

Detalhes do sistema de compilação avançado (corretor de manifesto)

Às vezes, as tags <uses-library> estão ausentes do manifesto de origem de uma biblioteca ou um app. Isso pode acontecer, por exemplo, se uma das dependências transitivas da biblioteca ou do app começar a usar outra tag <uses-library> e o manifesto da biblioteca ou do app não for atualizado para incluí-la.

O Soong pode calcular automaticamente algumas das tags <uses-library> ausentes para uma determinada biblioteca ou app, já que as bibliotecas do SDK no fechamento de dependências transitivo da biblioteca ou do app são necessários. O fechamento é necessário porque a biblioteca (ou app) pode depender de uma biblioteca estática que depende de uma biblioteca de SDK e, possivelmente, pode depender de novo de maneira transitiva por outra biblioteca.

Nem todas as tags <uses-library> podem ser calculadas dessa maneira, mas, quando possível, é preferível permitir que o Soong adicione entradas de manifesto automaticamente. Esse método é menos propenso a erros e simplifica a manutenção. Por exemplo, quando muitos apps usam uma biblioteca estática que adiciona uma nova dependência <uses-library>, todos os apps precisam ser atualizados, o que é difícil de manter.