Использование новых API

На этой странице объясняется, как ваше приложение может использовать новые функции ОС при работе в новых версиях ОС, сохраняя при этом совместимость со старыми устройствами.

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

По этой причине NDK не позволит вам создавать строгие ссылки на API, которые более новые, чем minSdkVersion вашего приложения. Это защитит вас от случайной отправки кода, который работал во время тестирования, но не смог загрузиться ( UnsatisfiedLinkError будет выброшен из System.loadLibrary() ) на старых устройствах. С другой стороны, сложнее написать код, использующий более новые API, чем minSdkVersion вашего приложения, поскольку вам придется вызывать API с помощью dlopen() и dlsym() а не обычного вызова функции.

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

Альтернативно, в справочнике по API NDK для каждого API будет указано что-то вроде «Введено в API 30». Если этот текст отсутствует, это означает, что 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

Clang очень внимательно относится к использованию __builtin_available . Работает только литерал (хотя, возможно, замененный макросом) if (__builtin_available(...)) . Даже тривиальные операции, такие как if (!__builtin_available(...)) не будут работать (Clang выдаст предупреждение unsupported-availability-guard , а также unguarded-availability ). Это может улучшиться в будущей версии Clang. Дополнительную информацию см. в выпуске LLVM 33161 .

Проверки unguarded-availability применяются только к той области функций, где они используются. Clang выдаст предупреждение, даже если функция с вызовом API вызывается только из защищенной области. Чтобы избежать повторения защитных мер в вашем собственном коде, см. раздел Как избежать повторения защитных мер API .

Почему это не значение по умолчанию?

Если не использовать их правильно, разница между сильными ссылками API и слабыми ссылками API заключается в том, что первая приведет к сбою быстро и очевидно, тогда как вторая не даст сбоя до тех пор, пока пользователь не выполнит действие, которое приведет к вызову отсутствующего API. Когда это произойдет, сообщение об ошибке не будет явной ошибкой времени компиляции «AFoo_bar() недоступен», это будет ошибка сегмента. При наличии сильных ссылок сообщение об ошибке становится намного яснее, а быстрый отказ является более безопасным значением по умолчанию.

Поскольку это новая функция, для безопасной обработки такого поведения написано очень мало существующего кода. Сторонний код, который был написан не с учетом Android, скорее всего, всегда будет иметь эту проблему, поэтому в настоящее время нет планов по изменению поведения по умолчанию.

Мы рекомендуем вам использовать это, но поскольку это затруднит обнаружение и отладку проблем, вам следует сознательно принять эти риски, а не менять поведение без вашего ведома.

Предостережения

Эта функция работает для большинства API, но в некоторых случаях она не работает.

Наименее вероятно, что возникнут проблемы с новыми API-интерфейсами libc. В отличие от остальных API Android, они защищены #if __ANDROID_API__ >= X в заголовках, а не только __INTRODUCED_IN(X) , что предотвращает просмотр даже слабого объявления. Поскольку самым старым уровнем API, поддерживаемым современными NDK, является r21, наиболее часто необходимые API-интерфейсы libc уже доступны. Новые API-интерфейсы libc добавляются в каждый выпуск (см. status.md ), но чем они новее, тем больше вероятность того, что они окажутся крайним случаем, который понадобится немногим разработчикам. Тем не менее, если вы один из этих разработчиков, вам нужно продолжать использовать dlsym() для вызова этих API, если ваша minSdkVersion старше API. Это решаемая проблема, но это сопряжено с риском нарушения совместимости исходного кода для всех приложений (любой код, содержащий полифилы API-интерфейсов libc, не удастся скомпилировать из-за несовпадения атрибутов availability в объявлениях libc и локальных объявлениях), поэтому мы не уверен, исправим ли мы это и когда.

Случай, с которым, скорее всего, столкнется больше разработчиков, — это когда библиотека , содержащая новый API, новее, чем ваша minSdkVersion . Эта функция допускает только слабые ссылки на символы; не существует такой вещи, как слабая ссылка на библиотеку. Например, если ваша minSdkVersion равна 24, вы можете связать libvulkan.so и выполнить защищенный вызов vkBindBufferMemory2 , поскольку libvulkan.so доступен на устройствах, начиная с API 24. С другой стороны, если ваша minSdkVersion была 23, вы должны упасть вернемся к dlopen и dlsym потому что библиотека не будет существовать на устройстве на устройствах, поддерживающих только API 23. Мы не знаем хорошего решения для исправления этого случая, но в долгосрочной перспективе это разрешится само собой, потому что мы (когда это возможно) ) больше не позволяет новым API создавать новые библиотеки.

Для авторов библиотеки

Если вы разрабатываете библиотеку для использования в приложениях Android, вам следует избегать использования этой функции в общедоступных заголовках. Его можно безопасно использовать во внешнем коде, но если вы полагаетесь на __builtin_available в любом коде ваших заголовков, например, во встроенных функциях или определениях шаблонов, вы заставляете всех своих потребителей включить эту функцию. По тем же причинам, по которым мы не включаем эту функцию по умолчанию в NDK, вам не следует делать этот выбор от имени своих потребителей.

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