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. Для последующих загрузок это нормально, поскольку приложения могут быть обработаны с помощью 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 во время загрузки с последующим dexopt).

Путь миграции

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

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

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

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

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

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

Если система сборки не может найти путь к DEX-файлу <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

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

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

PackageManager знает только о разделяемых библиотеках. Определение слова «разделяемая» в данном контексте отличается от его обычного значения (например, «разделяемая» против «статическая»). В Android разделяемые библиотеки Java — это те, которые перечислены в XML-конфигурациях, установленных на устройстве ( /system/etc/permissions/platform.xml ). Каждая запись содержит имя разделяемой библиотеки, путь к её DEX-файлу jar и список зависимостей (другие разделяемые библиотеки, которые эта библиотека использует во время выполнения и указывает в тегах <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 = '

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

logcat | grep -E 'ClassLoaderContext [az ]+ mismatch'

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

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

Подробная информация о системе сборки (средство для исправления манифеста)

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

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

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