Verificaciones de Dexpreopt y <uses-library>

Android 12 tiene cambios en el sistema de compilación para la compilación anticipada de archivos DEX (dexpreopt) para módulos de Java que tienen dependencias <uses-library>. En algunos casos, estos cambios del sistema de compilación pueden interrumpir las compilaciones. Usa esta página a fin de prepararte para las fallas y sigue las recetas que se indican en la página para corregirlas y mitigarlas.

Dexpreopt es el proceso de compilación anticipada de apps y bibliotecas de Java. Dexpreopt se realiza en el host en el momento de la compilación (a diferencia de dexopt, que se realiza en el dispositivo). La estructura de dependencias de bibliotecas compartidas que usa un módulo de Java (una biblioteca o una app) se conoce como su contexto de cargador de clases (CLC). Para garantizar la precisión de dexpreopt, los CLC del tiempo de compilación y de ejecución deben coincidir. El CLC del tiempo de compilación es lo que usa el compilador dex2oat en el tiempo de dexpreopt (se registra en los archivos ODEX) y el CLC del tiempo de ejecución es el contexto en el que se carga el código precompilado en el dispositivo.

Estos CLC de tiempo de compilación y tiempo de ejecución deben coincidir por motivos de exactitud y rendimiento. Para que sea correcto, es necesario controlar las clases duplicadas. Si las dependencias de las bibliotecas compartidas en el tiempo de ejecución son diferentes de las que se usan para la compilación, algunas de las clases podrían resolverse de manera diferente, lo que provocaría errores sutiles en el tiempo de ejecución. El rendimiento también se ve afectado por las verificaciones del tiempo de ejecución en busca de clases duplicadas.

Casos de uso afectados

El primer inicio es el caso de uso principal que se ve afectado por estos cambios: si ART detecta una discrepancia entre los CLC del tiempo de compilación y del tiempo de ejecución, rechaza los artefactos de dexpreopt y, en su lugar, ejecuta dexopt. Para los inicios posteriores, esto es correcto porque las apps se pueden dexoptar en segundo plano y almacenarse en el disco.

Áreas afectadas de Android

Esto afecta a todas las apps y bibliotecas de Java que tienen dependencias de entorno de ejecución en otras bibliotecas de Java. Android tiene miles de apps, y cientos de ellas usan bibliotecas compartidas. Los socios también se ven afectados, ya que tienen sus propias bibliotecas y apps.

Cambios en la pausa

El sistema de compilación debe conocer las dependencias de <uses-library> antes de generar las reglas de compilación de dexpreopt. Sin embargo, no puede acceder al manifiesto directamente ni leer las etiquetas <uses-library> que contiene, ya que el sistema de compilación no tiene permitido leer archivos arbitrarios cuando genera reglas de compilación (por motivos de rendimiento). Además, el manifiesto puede estar empaquetado dentro de un APK o una versión compilada previamente. Por lo tanto, la información de <uses-library> debe estar presente en los archivos de compilación (Android.bp o Android.mk).

Anteriormente, ART usaba una solución que ignoraba las dependencias de bibliotecas compartidas (conocidas como &-classpath). Esto no era seguro y causaba errores sutiles, por lo que se quitó la solución en Android 12.

Como resultado, los módulos de Java que no proporcionan información correcta de <uses-library> en sus archivos de compilación pueden causar pérdidas de compilación (causadas por una discrepancia de CLC en el tiempo de compilación) o regresión en el primer inicio (causada por una discrepancia de CLC en el tiempo de inicio seguida de dexopt).

Ruta de migración

Sigue estos pasos para corregir una compilación dañada:

  1. Configura para inhabilitar a nivel global la verificación del tiempo de compilación para un producto específico

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    en el archivo makefile del producto. Esto corrige los errores de compilación (excepto los casos especiales que se enumeran en la sección Cómo corregir las fallas). Sin embargo, esta es una solución temporal y puede causar una discrepancia de CLC en el tiempo de inicio seguida de dexopt.

  2. Para corregir los módulos que fallaron antes de inhabilitar globalmente la verificación del tiempo de compilación, agrega la información <uses-library> necesaria a sus archivos de compilación (consulta Cómo corregir fallas para obtener más información). Para la mayoría de los módulos, esto requiere agregar algunas líneas en Android.bp o en Android.mk.

  3. Inhabilita la verificación del tiempo de compilación y la dexpreopt para los casos problemáticos por módulo. Inhabilita dexpreopt para no desperdiciar tiempo de compilación ni almacenamiento en artefactos que se rechazan durante el inicio.

  4. Para volver a habilitar de forma global la verificación del tiempo de compilación, anula la configuración de PRODUCT_BROKEN_VERIFY_USES_LIBRARIES que se estableció en el paso 1. La compilación no debería fallar después de este cambio (debido a los pasos 2 y 3).

  5. Corrige los módulos que inhabilitaste en el paso 3, uno a la vez, y, luego, vuelve a habilitar dexpreopt y la verificación <uses-library>. Si es necesario, informa errores.

Las verificaciones de <uses-library> en el tiempo de compilación se aplican en Android 12.

Corregir fallas

En las siguientes secciones, se explica cómo corregir tipos específicos de fallas.

Error de compilación: Discrepancia de CLC

El sistema de compilación realiza una verificación de coherencia durante el tiempo de compilación entre la información de los archivos Android.bp o Android.mk y el manifiesto. El sistema de compilación no puede leer el manifiesto, pero puede generar reglas de compilación para leerlo (extraerlo de un APK si es necesario) y comparar las etiquetas <uses-library> del manifiesto con la información de <uses-library> en los archivos de compilación. Si la verificación falla, el error se verá de la siguiente manera:

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 sugiere el mensaje de error, existen varias soluciones, según la urgencia:

  • Para obtener una corrección temporal para todo el producto, establece PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true en el archivo makefile del producto. Aún se realiza la verificación de coherencia en el tiempo de compilación, pero una falla de verificación no significa un error de compilación. En cambio, una falla de verificación hace que el sistema de compilación cambie a una versión inferior el filtro del compilador dex2oat en verify en dexpreopt, que inhabilita la compilación AOT por completo para este módulo.
  • Para obtener una solución rápida y global de la línea de comandos, usa la variable de entorno RELAX_USES_LIBRARY_CHECK=true. Tiene el mismo efecto que PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, pero está diseñado para usarse en la línea de comandos. La variable de entorno anula la variable de producto.
  • Para obtener una solución que solucione la causa raíz del error, haz que el sistema de compilación conozca las etiquetas <uses-library> en el manifiesto. Una inspección del mensaje de error muestra qué bibliotecas causan el problema (al igual que inspeccionar AndroidManifest.xml o el manifiesto dentro de un APK que se puede verificar con "aapt dump badging $APK | grep uses-library").

Para módulos Android.bp:

  1. Busca la biblioteca faltante en la propiedad libs del módulo. Si está allí, Soong suele agregar esas bibliotecas automáticamente, excepto en los siguientes casos especiales:

    • La biblioteca no es una biblioteca de SDK (se define como java_library en lugar de java_sdk_library).
    • La biblioteca tiene un nombre de biblioteca diferente (en el manifiesto) que su nombre de módulo (en el sistema de compilación).

    Para corregir esto de forma temporal, agrega provides_uses_lib: "<library-name>" en la definición de la biblioteca Android.bp. Si quieres obtener una solución a largo plazo, corrige el problema subyacente: convierte la biblioteca en una biblioteca de SDK o cambia el nombre de su módulo.

  2. Si el paso anterior no proporcionó una resolución, agrega uses_libs: ["<library-module-name>"] para las bibliotecas requeridas o optional_uses_libs: ["<library-module-name>"] para las bibliotecas opcionales a la definición Android.bp del módulo. Estas propiedades aceptan una lista de nombres de módulos. El orden relativo de las bibliotecas en la lista debe ser el mismo que el del manifiesto.

Para módulos Android.mk:

  1. Verifica si la biblioteca tiene un nombre de biblioteca diferente (en el manifiesto) al nombre de su módulo (en el sistema de compilación). Si es así, soluciona este problema de forma temporal agregando LOCAL_PROVIDES_USES_LIBRARY := <library-name> en el archivo Android.mk de la biblioteca o agrega provides_uses_lib: "<library-name>" en el archivo Android.bp de la biblioteca (ambos casos son posibles, ya que un módulo Android.mk puede depender de una biblioteca Android.bp). Para obtener una solución a largo plazo, corrige el problema subyacente: cambia el nombre del módulo de biblioteca.

  2. Agrega LOCAL_USES_LIBRARIES := <library-module-name> para las bibliotecas requeridas y LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> para las bibliotecas opcionales a la definición Android.mk del módulo. Estas propiedades aceptan una lista de nombres de módulos. El orden relativo de las bibliotecas en la lista debe ser el mismo que el del manifiesto.

Error de compilación: ruta de acceso a la biblioteca desconocida

Si el sistema de compilación no puede encontrar una ruta a un archivo jar de DEX <uses-library> (ya sea una ruta de tiempo de compilación en el host o una ruta de instalación en el dispositivo), por lo general, falla la compilación. Si no se encuentra una ruta de acceso, es posible que se deba a que la biblioteca se configuró de forma inesperada. Corrige temporalmente la compilación inhabilitando dexpreopt para el módulo problemático.

Android.bp (propiedades del módulo):

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

Android.mk (variables de módulo):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Informa un error para investigar las situaciones no admitidas.

Error de compilación: Falta una dependencia de biblioteca.

Un intento de agregar <uses-library> X desde el manifiesto del módulo Y al archivo de compilación de Y puede generar un error de compilación debido a la dependencia faltante, X.

Este es un ejemplo de mensaje de error para módulos de Android.bp:

"Y" depends on undefined module "X"

Este es un ejemplo de mensaje de error 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

Una fuente común de estos errores ocurre cuando una biblioteca tiene un nombre diferente al de su módulo correspondiente en el sistema de compilación. Por ejemplo, si la entrada <uses-library> del manifiesto es com.android.X, pero el nombre del módulo de biblioteca es solo X, se producirá un error. Para resolver este caso, dile al sistema de compilación que el módulo llamado X proporciona un <uses-library> llamado com.android.X.

Este es un ejemplo de bibliotecas Android.bp (propiedad del módulo):

provides_uses_lib: “com.android.X”,

Este es un ejemplo de bibliotecas de Android.mk (variable de módulo):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Discrepancia en el CLC de inicio

En el primer inicio, busca en logcat mensajes relacionados con la falta de coincidencia de CLC, como se muestra a continuación:

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

El resultado puede tener mensajes del formato que se muestra aquí:

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

Si recibes una advertencia de discrepancia de CLC, busca un comando dexopt para el módulo defectuoso. Para solucionarlo, asegúrate de que se apruebe la verificación del tiempo de compilación del módulo. Si eso no funciona, es posible que el tuyo sea un caso especial que no sea compatible con el sistema de compilación (como una app que carga otro APK, no una biblioteca). El sistema de compilación no controla todos los casos, ya que, durante el tiempo de compilación, es imposible saber con certeza lo que carga la app durante el tiempo de ejecución.

Contexto del cargador de clases

El CLC es una estructura en forma de árbol que describe la jerarquía del cargador de clases. El sistema de compilación usa CLC de forma estrecha (solo abarca bibliotecas, no APK ni cargadores de clases personalizadas): es un árbol de bibliotecas que representa el cierre transitivo de todas las dependencias <uses-library> de una biblioteca o app. Los elementos de nivel superior de un CLC son las dependencias <uses-library> directas especificadas en el manifiesto (la ruta de clase). Cada nodo de un árbol de CLC es un nodo <uses-library> que puede tener sus propios subnodos <uses-library>.

Debido a que las dependencias de <uses-library> son un grafo acíclico dirigido y no necesariamente un árbol, el CLC puede contener varios subárboles para la misma biblioteca. En otras palabras, CLC es el gráfico de dependencia “desplegado” en un árbol. La duplicación solo está en un nivel lógico; los cargadores de clases subyacentes reales no están duplicados (en el tiempo de ejecución, hay una sola instancia de cargador de clases para cada biblioteca).

El CLC define el orden de búsqueda de las bibliotecas cuando resuelve las clases de Java que usa la biblioteca o la app. El orden de búsqueda es importante porque las bibliotecas pueden contener clases duplicadas, y la clase se resuelve en la primera coincidencia.

CLC integrado en el dispositivo (tiempo de ejecución)

PackageManager (en frameworks/base) crea un CLC para cargar un módulo de Java en el dispositivo. Este agrega las bibliotecas enumeradas en las etiquetas <uses-library> en el manifiesto del módulo como elementos de CLC de nivel superior.

Para cada biblioteca utilizada, PackageManager obtiene todas sus dependencias de <uses-library> (especificadas como etiquetas en el manifiesto de esa biblioteca) y agrega un CLC anidado para cada dependencia. Este proceso continúa de manera recursiva hasta que todos los nodos hoja del árbol de CLC construido sean bibliotecas sin dependencias <uses-library>.

PackageManager solo reconoce las bibliotecas compartidas. La definición de compartido en este uso difiere de su significado habitual (como compartido frente a estático). En Android, las bibliotecas compartidas de Java son aquellas que se enumeran en las configuraciones XML que se instalan en el dispositivo (/system/etc/permissions/platform.xml). Cada entrada contiene el nombre de una biblioteca compartida, una ruta de acceso a su archivo JAR DEX y una lista de dependencias (otras bibliotecas compartidas que esta usa en el tiempo de ejecución y especifica en las etiquetas <uses-library> de su manifiesto).

En otras palabras, hay dos fuentes de información que permiten que PackageManager compile CLC en el tiempo de ejecución: etiquetas <uses-library> en el manifiesto y dependencias de bibliotecas compartidas en configuraciones XML.

CLC en el host (en el momento de la compilación)

El CLC no solo es necesario cuando se carga una biblioteca o una app, sino también cuando se compila una. La compilación puede ocurrir en el dispositivo (dexopt) o durante la compilación (dexpreopt). Dado que el dexopt se realiza en el dispositivo, tiene la misma información que PackageManager (manifiestos y dependencias de bibliotecas compartidas). Sin embargo, Dexpreopt se realiza en el host y en un entorno totalmente diferente, y debe obtener la misma información del sistema de compilación.

Por lo tanto, el CLC de tiempo de compilación que usa dexpreopt y el CLC de tiempo de ejecución que usa PackageManager son lo mismo, pero se calculan de dos maneras diferentes.

Los CLC en el tiempo de compilación y en el entorno de ejecución deben coincidir; de lo contrario, se rechaza el código compilado por AOT que creó dexpreopt. Para comprobar la igualdad de los CLC en el tiempo de compilación y el tiempo de ejecución, el compilador dex2oat registra los CLC de tiempo de compilación en los archivos *.odex (en el campo classpath del encabezado del archivo OAT). Para encontrar el CLC almacenado, usa este comando:

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

La discrepancia de CLC en el tiempo de compilación y el tiempo de ejecución se informa en logcat durante el inicio. Para buscarlo, usa el siguiente comando:

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

La discrepancia es perjudicial para el rendimiento, ya que obliga a que la biblioteca o la app se dexopten o se ejecuten sin optimizaciones (por ejemplo, es posible que el código de la app deba extraerse en la memoria del APK, una operación muy costosa).

Una biblioteca compartida puede ser opcional o obligatoria. Desde el punto de vista de dexpreopt, una biblioteca obligatoria debe estar presente en el momento de la compilación (su ausencia es un error de compilación). Una biblioteca opcional puede estar presente o ausente en el momento de la compilación: si está presente, se agrega al CLC, se pasa a dex2oat y se registra en el archivo *.odex. Si falta una biblioteca opcional, se omite y no se agrega al CLC. Si hay una discrepancia entre el tiempo de compilación y el estado de ejecución (la biblioteca opcional está presente en un caso, pero no en el otro), los CLC de tiempo de compilación y de ejecución no coinciden y se rechaza el código compilado.

Detalles avanzados del sistema de compilación (corrector de manifiestos)

A veces, faltan etiquetas <uses-library> en el manifiesto de origen de una biblioteca o app. Esto puede ocurrir, por ejemplo, si una de las dependencias transitivas de la biblioteca o app comienza a usar otra etiqueta <uses-library> y el manifiesto de la biblioteca o la app no se actualiza para incluirla.

Soong puede procesar automáticamente algunas de las etiquetas <uses-library> faltantes de una biblioteca o app determinada, como las bibliotecas de SDK en el cierre de dependencia transitiva de la biblioteca o app. El cierre es necesario porque la biblioteca (o app) puede depender de una biblioteca estática que depende de una biblioteca de SDK y, posiblemente, volver de forma transitiva a través de otra biblioteca.

No todas las etiquetas <uses-library> se pueden procesar de esta manera, pero, cuando sea posible, es preferible permitir que Soong agregue entradas de manifiesto automáticamente. Es menos propenso a errores y simplifica el mantenimiento. Por ejemplo, cuando muchas apps usan una biblioteca estática que agrega una nueva dependencia <uses-library>, todas las apps deben estar actualizadas, lo cual es difícil de mantener.