На этой странице объясняется, как ваше приложение может использовать новые функциональные возможности ОС при работе на новых версиях ОС, сохраняя при этом совместимость со старыми устройствами.
По умолчанию ссылки на API NDK в вашем приложении являются сильными ссылками. Динамический загрузчик Android будет с нетерпением пытаться разрешить их при загрузке вашей библиотеки. Если символы не будут найдены, приложение прервется. Это противоречит поведению Java, где исключение не будет выдано, пока не будет вызван отсутствующий API.
По этой причине NDK не позволит вам создавать сильные ссылки на API, которые новее minSdkVersion
вашего приложения. Это защищает вас от случайной отправки кода, который работал во время тестирования, но не загрузится (будет выдана UnsatisfiedLinkError
из System.loadLibrary()
) на старых устройствах. С другой стороны, сложнее написать код, который использует API новее minSdkVersion
вашего приложения, поскольку вы должны вызывать API с помощью dlopen()
и dlsym()
, а не обычного вызова функции.
Альтернативой использованию сильных ссылок является использование слабых ссылок. Если слабая ссылка не найдена при загрузке библиотеки, адрес этого символа будет установлен в nullptr
вместо того, чтобы вызвать сбой загрузки библиотеки. Их по-прежнему нельзя безопасно вызывать, но пока callsites защищены от вызова API, когда он недоступен, остальная часть вашего кода может быть запущена, и вы можете вызывать API обычным образом, без необходимости использования dlopen()
и dlsym()
.
Слабые ссылки API не требуют дополнительной поддержки со стороны динамического компоновщика, поэтому их можно использовать с любой версией Android.
Включение слабых ссылок API в вашу сборку
CMake
Передайте -DANDROID_WEAK_API_DEFS=ON
при запуске CMake. Если вы используете CMake через externalNativeBuild
, добавьте следующее в ваш build.gradle.kts
(или эквивалент Groovy, если вы все еще используете build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-сборка
Добавьте следующее в файл Application.mk
:
APP_WEAK_API_DEFS := true
Если у вас еще нет файла Application.mk
, создайте его в том же каталоге, что и файл Android.mk
. Дополнительные изменения в файле build.gradle.kts
(или build.gradle
) не нужны для ndk-build.
Другие системы сборки
Если вы не используете CMake или ndk-build, обратитесь к документации вашей системы сборки, чтобы узнать, есть ли рекомендуемый способ включить эту функцию. Если ваша система сборки изначально не поддерживает эту опцию, вы можете включить ее, передав следующие флаги при компиляции:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
Первый настраивает заголовки NDK для разрешения слабых ссылок. Второй превращает предупреждение о небезопасных вызовах API в ошибку.
Более подробную информацию см. в Руководстве для специалистов по обслуживанию систем сборки .
Защищенные вызовы API
Эта функция не делает вызовы новых API волшебным образом безопасными. Единственное, что она делает, — это откладывает ошибку загрузки до ошибки вызова. Преимущество в том, что вы можете защитить этот вызов во время выполнения и изящно откатиться назад, используя альтернативную реализацию или уведомляя пользователя о том, что эта функция приложения недоступна на его устройстве, или полностью избегая этого пути кода.
Clang может выдать предупреждение ( unguarded-availability
), когда вы делаете незащищенный вызов API, который недоступен для minSdkVersion
вашего приложения. Если вы используете ndk-build или наш файл цепочки инструментов CMake, это предупреждение будет автоматически включено и преобразовано в ошибку при включении этой функции.
Вот пример кода, который условно использует API без включенной этой функции, используя dlopen()
и dlsym()
:
void LogImageDecoderResult(int result) {
void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
dlsym(lib, "AImageDecoder_resultToString")
);
if (func == nullptr) {
LOG(INFO) << "cannot stringify result: " << result;
} else {
LOG(INFO) << func(result);
}
}
Его немного неудобно читать, есть некоторое дублирование имен функций (а если вы пишете на C, то и сигнатур тоже), он будет успешно собран, но всегда будет использовать резервный вариант во время выполнения, если вы случайно опечатаетесь в имени функции, переданном в dlsym
, и вам придется использовать этот шаблон для каждого API.
При слабых ссылках на API приведенную выше функцию можно переписать следующим образом:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
Под капотом __builtin_available(android 31, *)
вызывает android_get_device_api_level()
, кэширует результат и сравнивает его с 31
(который является уровнем API, который представил AImageDecoder_resultToString()
).
Самый простой способ определить, какое значение использовать для __builtin_available
— попытаться выполнить сборку без защиты (или защиты __builtin_available(android 1, *)
) и сделать то, что вам скажет сообщение об ошибке. Например, незащищенный вызов AImageDecoder_createFromAAsset()
с minSdkVersion 24
даст:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
В этом случае вызов должен быть защищен __builtin_available(android 30, *)
. Если ошибки сборки нет, то либо API всегда доступен для вашего minSdkVersion
и защита не требуется, либо ваша сборка неправильно настроена и предупреждение unguarded-availability
отключено.
В качестве альтернативы, в справочнике NDK API будет указано что-то вроде "Введено в API 30" для каждого API. Если этот текст отсутствует, это означает, что API доступен для всех поддерживаемых уровней API.
Избежание повторения защит API
Если вы используете это, у вас, вероятно, будут разделы кода в вашем приложении, которые можно использовать только на достаточно новых устройствах. Вместо того, чтобы повторять проверку __builtin_available()
в каждой из ваших функций, вы можете аннотировать свой собственный код как требующий определенного уровня API. Например, сами API ImageDecoder были добавлены в API 30, поэтому для функций, которые интенсивно используют эти API, вы можете сделать что-то вроде:
#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)
void DecodeImageWithImageDecoder() REQUIRES_API(30) {
// Call any APIs that were introduced in API 30 or newer without guards.
}
void DecodeImageFallback() {
// Pay the overhead to call the Java APIs via JNI, or use third-party image
// decoding libraries.
}
void DecodeImage() {
if (API_AT_LEAST(30)) {
DecodeImageWithImageDecoder();
} else {
DecodeImageFallback();
}
}
Особенности API Guards
Clang очень требователен к использованию __builtin_available
. Работает только литерал (хотя, возможно, и макрозамененный) if (__builtin_available(...))
. Даже такие тривиальные операции, как if (!__builtin_available(...))
не будут работать (Clang выдаст предупреждение unsupported-availability-guard
, а также unguarded-availability
). Это может быть улучшено в будущей версии Clang. Для получения дополнительной информации см. LLVM Issue 33161.
Проверки на unguarded-availability
применяются только к области действия функции, где они используются. Clang выдаст предупреждение, даже если функция с вызовом API вызывается только из защищенной области действия. Чтобы избежать повторения защит в вашем собственном коде, см. раздел Избегание повторения защит API .
Почему это не значение по умолчанию?
Если не использовать их правильно, разница между сильными и слабыми ссылками API заключается в том, что первые быстро и очевидно откажут, тогда как вторые не откажут, пока пользователь не предпримет действие, которое вызовет вызов отсутствующего API. Когда это происходит, сообщение об ошибке не будет явной ошибкой времени компиляции "AFoo_bar() is not available", это будет segfault. При сильных ссылках сообщение об ошибке гораздо яснее, а failing-fast является более безопасным значением по умолчанию.
Поскольку это новая функция, очень мало существующего кода написано для безопасной обработки этого поведения. Сторонний код, который не был написан с учетом Android, скорее всего, всегда будет иметь эту проблему, поэтому в настоящее время нет планов по изменению поведения по умолчанию.
Мы рекомендуем вам использовать эту функцию, но поскольку это затруднит обнаружение и устранение неполадок, вам следует сознательно принять эти риски, а не допускать, чтобы поведение изменилось без вашего ведома.
Предостережения
Эта функция работает для большинства API, но есть несколько случаев, когда она не работает.
До версии NDK r28 это не работало для API libc или libm.
Случай, с которым, скорее всего, столкнется больше разработчиков, — это когда библиотека , содержащая новый API, новее, чем ваш minSdkVersion
. Эта функция разрешает только слабые ссылки на символы; слабой ссылки на библиотеку не существует. Например, если ваш minSdkVersion
равен 24, вы можете связать libvulkan.so
и сделать защищенный вызов vkBindBufferMemory2
, поскольку libvulkan.so
доступен на устройствах, начинающихся с API 24. С другой стороны, если ваш minSdkVersion
равен 23, вы должны вернуться к dlopen
и dlsym
поскольку библиотека не будет существовать на устройстве на устройствах, которые поддерживают только API 23. Мы не знаем хорошего решения для исправления этого случая, но в долгосрочной перспективе он разрешится сам собой, поскольку мы (когда это возможно) больше не разрешаем новым API создавать новые библиотеки.
Для авторов библиотек
Если вы разрабатываете библиотеку для использования в приложениях Android, вам следует избегать использования этой функции в ваших публичных заголовках. Ее можно безопасно использовать в коде out-of-line, но если вы полагаетесь на __builtin_available
в любом коде в ваших заголовках, например, во встроенных функциях или определениях шаблонов, вы заставляете всех своих потребителей включать эту функцию. По тем же причинам, по которым мы не включаем эту функцию по умолчанию в NDK, вам следует избегать принятия такого решения от имени ваших потребителей.
Если вы требуете такого поведения в своих публичных заголовках, обязательно задокументируйте это, чтобы ваши пользователи знали, что им нужно будет включить эту функцию, и осознавали связанные с этим риски.