Проверки dexpreopt и <uses-library>

В Android 12 внесены изменения в систему сборки, касающиеся AOT-компиляции DEX-файлов (dexpreopt) для модулей Java, у которых есть зависимости <uses-library>. В некоторых случаях эти изменения могут привести к сбоям при сборке. На этой странице рассказывается, как подготовиться к таким ситуациям, а также как устранять и предотвращать их.

Dexpreopt – это процесс предварительной компиляции библиотек и приложений Java. Dexpreopt выполняется на хосте во время сборки (в отличие от dexopt, который выполняется на устройстве). Структура зависимостей общей библиотеки, используемой модулем Java (библиотекой или приложением), называется контекстом загрузчика классов (CLC). Чтобы гарантировать корректность dexpreopt, CLC времени сборки и времени выполнения должны совпадать. CLC времени сборки используется компилятором dex2oat во время dexpreopt (записывается в файлы ODEX), а CLC времени выполнения – это контекст, в котором предварительно скомпилированный код загружается на устройство.

Эти CLC должны совпадать во время сборки и выполнения, чтобы обеспечить корректность и производительность. Чтобы обеспечить корректность, необходимо обрабатывать дублирующиеся классы. Если зависимости общей библиотеки во время выполнения отличаются от тех, которые использовались для компиляции, некоторые классы могут быть разрешены по-другому, что приведет к незначительным ошибкам во время выполнения. На производительность также влияют проверки на наличие повторяющихся классов во время выполнения.

Затронутые варианты использования

Первая загрузка – основной сценарий использования, на который влияют эти изменения. Если ART обнаруживает несоответствие между CLC во время сборки и во время выполнения, он отклоняет артефакты dexpreopt и запускает dexopt. При последующих загрузках это не проблема, поскольку приложения можно оптимизировать в фоновом режиме и сохранить на диск.

Затронутые области Android

Это влияет на все приложения и библиотеки Java, которые во время выполнения зависят от других библиотек Java. Для Android созданы тысячи приложений, и сотни из них используют общие библиотеки. Это также касается партнеров, у которых есть собственные библиотеки и приложения.

Критические изменения

Система сборки должна знать зависимости <uses-library>, прежде чем генерировать правила сборки dexpreopt. Однако она не может напрямую получить доступ к манифесту и прочитать теги <uses-library> в нем, поскольку системе сборки не разрешено читать произвольные файлы при создании правил сборки (из соображений производительности). Кроме того, манифест может быть упакован в APK-файл или встроен в образ. Поэтому информация <uses-library> должна быть в файлах сборки (Android.bp или Android.mk).

Ранее ART использовал обходной путь, который игнорировал зависимости от общих библиотек (известные как &-classpath). Это было небезопасно и приводило к незаметным ошибкам, поэтому в Android 12 обходной путь был удален.

В результате модули Java, в файлах сборки которых указана неверная информация о <uses-library>, могут привести к сбоям сборки (из-за несоответствия CLC во время сборки) или регрессии времени первой загрузки (из-за несоответствия CLC во время загрузки с последующей оптимизацией dex).

Путь переноса

Чтобы исправить неработающую сборку, выполните следующие действия:

  1. Чтобы отключить проверку во время сборки для определенного продукта, задайте

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    в файле makefile продукта. Это позволяет устранить ошибки сборки (за исключением особых случаев, перечисленных в разделе Устранение неполадок). Однако это временное решение, которое может привести к несоответствию CLC во время загрузки и последующей оптимизации dexopt.

  2. Исправьте модули, которые не прошли проверку, прежде чем отключать ее для всего проекта. Для этого добавьте в файлы сборки необходимую информацию <uses-library> (подробнее о том, как устранять ошибки…). Для большинства модулей это требует добавления нескольких строк в Android.bp или Android.mk.

  3. Отключите проверку во время сборки и dexpreopt для проблемных случаев на уровне модулей. Отключите dexpreopt, чтобы не тратить время сборки и место в хранилище на артефакты, которые отклоняются при загрузке.

  4. Снова включите проверку во время сборки, удалив переменную PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, заданную на шаге 1. После этого сборка должна пройти успешно (благодаря шагам 2 и 3).

  5. Исправьте модули, которые вы отключили на шаге 3, по одному, а затем снова включите dexpreopt и проверку <uses-library>. При необходимости сообщите об ошибках.

В Android 12 во время сборки выполняются проверки <uses-library>.

Как устранять неполадки

В следующих разделах рассказывается, как устранить определенные типы неполадок.

Ошибка сборки: несоответствие CLC

Система сборки во время сборки проверяет согласованность информации в файлах Android.bp или Android.mk и манифесте. Система сборки не может прочитать манифест, но может создать правила сборки для его чтения (при необходимости извлекая его из APK) и сравнить теги <uses-library> в манифесте с информацией <uses-library> в файлах сборки. Если проверка не пройдена, ошибка выглядит следующим образом:

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

Как указано в сообщении об ошибке, есть несколько решений в зависимости от срочности:

  • Чтобы временно исправить проблему на уровне всего продукта, задайте значение PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true в файле makefile продукта. Проверка согласованности во время сборки по-прежнему выполняется, но ее сбой не означает сбой сборки. Вместо этого при сбое проверки система сборки понижает фильтр компилятора dex2oat до verify в dexpreopt, что полностью отключает AOT-компиляцию для этого модуля.
  • Чтобы быстро устранить проблему с помощью командной строки, используйте переменную среды RELAX_USES_LIBRARY_CHECK=true. Она имеет тот же эффект, что и команда PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, но предназначена для использования в командной строке. Переменная среды переопределяет переменную продукта.
  • Чтобы устранить основную причину ошибки, добавьте теги <uses-library> в манифест. Из сообщения об ошибке можно узнать, какие библиотеки вызывают проблему (как и из файла AndroidManifest.xml или манифеста в APK, который можно проверить с помощью команды aapt dump badging $APK | grep uses-library).

Для модулей Android.bp:

  1. Найдите недостающую библиотеку в свойстве libs модуля. Если она есть, Soong обычно добавляет такие библиотеки автоматически, за исключением следующих особых случаев:

    • Библиотека не является библиотекой SDK (она определяется как java_library, а не java_sdk_library).
    • Название библиотеки в манифесте отличается от названия модуля в системе сборки.

    Чтобы временно устранить эту проблему, добавьте provides_uses_lib: "<library-name>" в определение библиотеки Android.bp. Чтобы устранить проблему на долгосрочной основе, преобразуйте библиотеку в библиотеку SDK или переименуйте ее модуль.

  2. Если предыдущий шаг не помог, добавьте uses_libs: ["<library-module-name>"] для обязательных библиотек или optional_uses_libs: ["<library-module-name>"] для необязательных библиотек в определение модуля Android.bp. Эти свойства принимают список названий модулей. Порядок библиотек в списке должен совпадать с порядком в манифесте.

Для модулей Android.mk:

  1. Проверьте, не отличается ли название библиотеки в манифесте от названия ее модуля в системе сборки. Если это так, временно исправьте проблему, добавив LOCAL_PROVIDES_USES_LIBRARY := <library-name> в файл Android.mk библиотеки или provides_uses_lib: "<library-name>" в файл Android.bp библиотеки (возможны оба варианта, поскольку модуль Android.mk может зависеть от библиотеки Android.bp). Чтобы устранить проблему на длительный срок, переименуйте модуль библиотеки.

  2. Добавьте LOCAL_USES_LIBRARIES := <library-module-name> для обязательных библиотек и LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> для необязательных библиотек в определение модуля Android.mk. Эти свойства принимают список названий модулей. Порядок библиотек в списке должен совпадать с порядком в манифесте.

Ошибка сборки: неизвестный путь к библиотеке.

Если система сборки не может найти путь к DEX-файлу JAR (путь во время сборки на хосте или путь установки на устройстве), сборка обычно завершается неудачно.<uses-library> Если путь не найден, возможно, библиотека настроена неправильно. Временно устраните проблему, отключив dexpreopt для проблемного модуля.

Android.bp (свойства модуля):

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

Android.mk (переменные модуля):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Если вы обнаружили неподдерживаемый сценарий, сообщите об ошибке.

Ошибка сборки: отсутствует зависимость библиотеки

Попытка добавить <uses-library> X из манифеста модуля Y в файл сборки для Y может привести к ошибке сборки из-за отсутствия зависимости X.

Вот пример сообщения об ошибке для модулей Android.bp:

"Y" depends on undefined module "X"

Вот пример сообщения об ошибке для модулей 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

Часто такие ошибки возникают, когда название библиотеки отличается от названия соответствующего модуля в системе сборки. Например, если запись манифеста <uses-library> – com.android.X, а название модуля библиотеки – X, это приведет к ошибке. Чтобы устранить эту проблему, укажите в системе сборки, что модуль X предоставляет <uses-library> с именем com.android.X.

Пример для библиотек Android.bp (свойство модуля):

provides_uses_lib: “com.android.X”,

Пример для библиотек Android.mk (переменная модуля):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Несоответствие CLC при загрузке

При первой загрузке найдите в logcat сообщения, связанные с несоответствием CLC, как показано ниже:

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

Выходные данные могут содержать сообщения в следующем формате:

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

Если вы получили предупреждение о несоответствии CLC, найдите команду dexopt для неисправного модуля. Чтобы устранить эту проблему, убедитесь, что проверка во время сборки для модуля пройдена. Если это не поможет, возможно, ваш случай особый и не поддерживается системой сборки (например, если приложение загружает другой APK, а не библиотеку). Система сборки не обрабатывает все случаи, поскольку во время сборки невозможно точно знать, что приложение загружает во время выполнения.

Контекст загрузчика классов

CLC – это древовидная структура, описывающая иерархию загрузчиков классов. Система сборки использует CLC в узком смысле (она охватывает только библиотеки, а не APK или загрузчики пользовательских классов): это дерево библиотек, представляющее транзитивное замыкание всех зависимостей <uses-library> библиотеки или приложения. Элементы верхнего уровня CLC – это прямые зависимости <uses-library>, указанные в манифесте (путь к классам). Каждый узел дерева CLC – это узел <uses-library>, у которого могут быть собственные дочерние узлы <uses-library>.

Поскольку <uses-library> зависимости представляют собой ориентированный ациклический граф, а не обязательно дерево, CLC может содержать несколько поддеревьев для одной и той же библиотеки. Другими словами, CLC – это граф зависимостей, "развернутый" в дерево. Дублирование происходит только на логическом уровне. Фактические загрузчики классов не дублируются (во время выполнения для каждой библиотеки существует только один экземпляр загрузчика классов).

CLC определяет порядок поиска библиотек при разрешении классов Java, используемых библиотекой или приложением. Порядок поиска важен, поскольку библиотеки могут содержать дублирующиеся классы, и класс разрешается в первое совпадение.

CLC на устройстве (во время выполнения)

PackageManager (в frameworks/base) создает CLC для загрузки модуля Java на устройство. Она добавляет библиотеки, перечисленные в тегах <uses-library> в манифесте модуля, в качестве элементов CLC верхнего уровня.

Для каждой используемой библиотеки PackageManager получает все ее зависимости <uses-library> (указанные в виде тегов в манифесте этой библиотеки) и добавляет вложенный CLC для каждой зависимости. Этот процесс продолжается рекурсивно, пока все конечные узлы построенного дерева CLC не станут библиотеками без зависимостей <uses-library>.

PackageManager знает только об общих библиотеках. В этом контексте термин "общий" имеет другое значение, чем обычно (например, в противопоставлении "общий" и "статический"). В Android общие библиотеки Java – это библиотеки, перечисленные в XML-файлах конфигурации, которые установлены на устройстве (/system/etc/permissions/platform.xml). Каждая запись содержит название общей библиотеки, путь к ее JAR-файлу DEX и список зависимостей (других общих библиотек, которые используются этой библиотекой во время выполнения и указаны в тегах <uses-library> в ее манифесте).

Другими словами, во время выполнения PackageManagerсоздает CLC на основе двух источников информации: тегов <uses-library> в манифесте и зависимостей общей библиотеки в конфигурациях XML.

CLC на хосте (во время сборки)

CLC нужен не только при загрузке библиотеки или приложения, но и при их компиляции. Компиляция может выполняться на устройстве (dexopt) или во время сборки (dexpreopt). Поскольку dexopt выполняется на устройстве, у него есть та же информация, что и у PackageManager (манифесты и зависимости от общих библиотек). Однако dexpreopt выполняется на хосте и в совершенно другой среде, и ему приходится получать ту же информацию из системы сборки.

Таким образом, CLC, используемый dexpreopt во время сборки, и CLC, используемый PackageManager во время выполнения, – это одно и то же, но вычисляемое двумя разными способами.

CLC, используемые во время сборки и выполнения, должны совпадать, иначе код, скомпилированный AOT с помощью dexpreopt, будет отклонен. Чтобы проверить равенство CLC времени сборки и времени выполнения, компилятор dex2oat записывает CLC времени сборки в файлы *.odex (в поле classpath заголовка файла OAT). Чтобы найти сохраненный код CLC, используйте следующую команду:

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

Несоответствие CLC во время сборки и выполнения сообщается в logcat во время загрузки. Найдите его, используя следующую команду:

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

Несоответствие плохо влияет на производительность, поскольку вынуждает библиотеку или приложение либо выполнять dexopt, либо работать без оптимизаций (например, код приложения может потребоваться извлечь из APK в память, что является очень дорогостоящей операцией).

Общая библиотека может быть обязательной или необязательной. С точки зрения dexpreopt, необходимая библиотека должна присутствовать во время сборки (ее отсутствие – ошибка сборки). Необязательная библиотека может присутствовать или отсутствовать во время сборки. Если она присутствует, то добавляется в CLC, передается в dex2oat и записывается в файл *.odex. Если необязательная библиотека отсутствует, она пропускается и не добавляется в CLC. Если статус во время сборки и во время выполнения не совпадает (например, необязательная библиотека присутствует в одном случае, но не в другом), то CLC во время сборки и во время выполнения не совпадают, и скомпилированный код отклоняется.

Расширенные сведения о системе сборки (исправление манифеста)

Иногда теги <uses-library> отсутствуют в исходном манифесте библиотеки или приложения. Это может произойти, например, если одна из транзитивных зависимостей библиотеки или приложения начинает использовать другой тег <uses-library>, а манифест библиотеки или приложения не обновляется для его включения.

Soong может автоматически вычислить некоторые отсутствующие теги <uses-library> для определенной библиотеки или приложения, поскольку библиотеки SDK в транзитивном замыкании зависимостей библиотеки или приложения. Замыкание необходимо, поскольку библиотека (или приложение) может зависеть от статической библиотеки, которая зависит от библиотеки SDK, и, возможно, снова зависеть транзитивно через другую библиотеку.

Не все теги <uses-library> можно вычислить таким образом, но если это возможно, лучше позволить Soong добавить записи манифеста автоматически. Это снижает вероятность ошибок и упрощает обслуживание. Например, если многие приложения используют статическую библиотеку, которая добавляет новую зависимость <uses-library>, все приложения необходимо обновить, что сложно сделать.