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 interromper os builds. Use esta página para se preparar para interrupções e siga as receitas nela para corrigir e reduzir as interrupções.

O dexpreopt é o processo de compilação antecipada de bibliotecas e apps Java. O Dexpreopt ocorre no host no momento da build, ao contrário do dexopt, que acontece no dispositivo. A estrutura de dependências de bibliotecas compartilhadas usada por um módulo Java (uma biblioteca ou um app) é conhecida como contexto do carregador de classes (CLC). Para garantir a correção de 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.

Esses CLCs de build e de execução precisam coincidir por motivos de correção e desempenho. Para garantir a correção, é necessário processar classes duplicadas. Se as dependências da biblioteca compartilhada no momento da execução forem diferentes daquelas usadas para a compilação, algumas das classes poderão ser resolvidas de maneira diferente, causando bugs sutis no momento da execução. O desempenho também é afetado pelas verificações 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 build-time e run-time, ele rejeitará artefatos dexpreopt e executará dexopt. Isso é aceitável para inicializações subsequentes, porque os apps podem ser desativados 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 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 de <uses-library> antes de gerar regras de build 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 quando gera 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 <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 de 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, módulos Java que não fornecem informações <uses-library> corretas nos arquivos de build podem causar quebras de build (causadas por uma incompatibilidade de CLC no momento do build) ou regressões no tempo de inicialização (causadas por uma incompatibilidade de CLC no momento da inicialização seguida de dexopt).

Caminho de migração

Siga estas etapas para corrigir um build corrompido:

  1. Desativar globalmente a verificação no tempo de build para um produto específico definindo

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

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

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

  3. Desative a verificação no build e o dexpreopt 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 globalmente a verificação no build desativando PRODUCT_BROKEN_VERIFY_USES_LIBRARIES que foi definido na etapa 1. O build não deve falhar após essa mudança (devido às etapas 2 e 3).

  5. Corrija os módulos desativados na etapa 3, um por vez, e reative o dexpreopt e a verificação <uses-library>. Registre bugs, se necessário.

As verificações <uses-library> no tempo de build 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 as tags <uses-library> no manifesto com as informações <uses-library> nos arquivos de build. Se a verificação falhar, o erro será semelhante a 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 no build ainda é realizada, mas uma falha na verificação não significa um erro 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 o 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:

    • A biblioteca não é uma biblioteca do SDK. Ela é definida como java_library em vez de java_sdk_library.
    • A biblioteca tem um nome 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. Verifique se a biblioteca tem um nome diferente (no manifesto) do nome do módulo (no sistema de build). Se isso acontecer, corrija temporariamente adicionando LOCAL_PROVIDES_USES_LIBRARY := <library-name> no arquivo Android.mk da biblioteca ou adicione provides_uses_lib: "<library-name>" no arquivo Android.bp da biblioteca. Ambos os 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 obrigatórias e 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 de 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. A falha na localização de um caminho pode indicar que a biblioteca está configurada de forma inesperada. Corrija temporariamente o build 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

Informe um bug para investigar os cenários sem suporte.

Erro de build: dependência de biblioteca ausente

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

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

"Y" depends on undefined module "X"

Confira um exemplo de mensagem de erro para módulos do 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 de bibliotecas Android.bp (propriedade do módulo):

provides_uses_lib: “com.android.X”,

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

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Incompatibilidade de CLC no momento da 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 defeituoso. 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 no momento do build é impossível saber com certeza o que o app carrega no momento da execução.

Contexto do carregador de classe

O CLC é uma estrutura em árvore que descreve a hierarquia do class loader. 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 seus próprios subnós <uses-library>.

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

O CLC define a ordem de pesquisa das bibliotecas ao resolver classes Java usadas pela biblioteca ou pelo app. A ordem de pesquisa é 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 um CLC para carregar um módulo Java no dispositivo. Ele adiciona as bibliotecas listadas nas tags <uses-library> no manifesto do módulo como elementos de CLC de nível superior.

Para cada biblioteca usada, 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>.

O PackageManager só reconhece bibliotecas compartilhadas. A definição de "compartilhado" nesse uso é diferente do significado usual (como "compartilhado" e "estático"). No Android, as bibliotecas compartilhadas Java são aquelas listadas em configurações XML que são 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 ambiente 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 tempo de 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). O Dexpreopt, no entanto, 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 pelo dexpreopt e o CLC no tempo de execução usado pelo PackageManager são a mesma coisa, mas calculados de duas maneiras diferentes.

Os CLCs no momento do build e da execução precisam coincidir. Caso contrário, o código compilado pelo AOT criado pelo dexpreopt 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 do CLC no momento de build e de execução é informada no logcat durante a inicialização. Pesquise por ele com este comando:

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

A incompatibilidade é ruim para a performance, 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 obrigatória precisa estar presente no momento do build. A ausência dela é um erro de build. Uma biblioteca opcional pode estar presente ou ausente no momento do build: se estiver presente, ela será adicionada ao CLC, transmitida para dex2oat e gravada no arquivo *.odex. Se uma biblioteca opcional estiver ausente, 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 avançados do sistema de build (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 algumas das tags <uses-library> ausentes de uma determinada biblioteca ou app automaticamente, como as bibliotecas do SDK no fechamento de dependência transitiva da biblioteca ou do app. O fechamento é necessário porque a biblioteca (ou o app) pode depender de uma biblioteca estática que depende de uma biblioteca do SDK e, possivelmente, pode depender transitivamente de outra biblioteca.

Nem todas as tags <uses-library> podem ser computadas dessa maneira, mas, quando possível, é preferível permitir que o Soong adicione entradas de manifesto automaticamente. Isso é 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 de <uses-library>, todos os apps precisam ser atualizados, o que é difícil de manter.