En esta página, se explica cómo tu app puede usar la nueva funcionalidad del SO cuando se ejecuta en versiones nuevas del SO y, al mismo tiempo, conservar la compatibilidad con dispositivos más antiguos.
De forma predeterminada, las referencias a las APIs de NDK en tu aplicación son referencias fuertes. El cargador dinámico de Android los resolverá con entusiasmo cuando se cargue la biblioteca. Si no se encuentran los símbolos, se abortará la app. Esto es contrario al comportamiento de Java, en el que no se arrojará una excepción hasta que se llame a la API faltante.
Por este motivo, el NDK evitará que crees referencias sólidas a APIs que sean más recientes que el minSdkVersion
de tu app. Esto te protege de enviar accidentalmente un código que funcionó durante las pruebas, pero que no se cargará (se arrojará UnsatisfiedLinkError
desde System.loadLibrary()
) en dispositivos más antiguos. Por otro lado, es más difícil escribir código que use APIs más nuevas que el minSdkVersion
de tu app, ya que debes llamar a las APIs con dlopen()
y dlsym()
en lugar de una llamada a función normal.
La alternativa a usar referencias sólidas es usar referencias débiles. Una referencia débil que no se encuentra cuando se carga la biblioteca hace que la dirección de ese símbolo se establezca en nullptr
en lugar de no cargarse. Aún así,
no se pueden llamar de forma segura, pero, siempre y cuando los sitios de llamada estén protegidos para evitar llamar
a la API cuando no está disponible, se puede ejecutar el resto del código y puedes llamar
a la API de forma normal sin necesidad de usar dlopen()
y dlsym()
.
Las referencias de API débiles no requieren compatibilidad adicional del vinculador dinámico, por lo que se pueden usar con cualquier versión de Android.
Habilita referencias de API débiles en tu compilación
CMake
Pasa -DANDROID_WEAK_API_DEFS=ON
cuando ejecutes CMake. Si usas CMake a través de externalNativeBuild
, agrega lo siguiente a tu build.gradle.kts
(o el equivalente de Groovy si aún usas build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
Agrega lo siguiente a tu archivo Application.mk
:
APP_WEAK_API_DEFS := true
Si aún no tienes un archivo Application.mk
, créalo en el mismo directorio que el archivo Android.mk
. No es necesario realizar cambios adicionales en el archivo build.gradle.kts
(o build.gradle
) para ndk-build.
Otros sistemas de compilaciones
Si no usas CMake o ndk-build, consulta la documentación de tu sistema de compilación para ver si hay una forma recomendada de habilitar esta función. Si tu sistema de compilación no admite esta opción de forma nativa, puedes habilitar la función pasando las siguientes marcas durante la compilación:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
El primero configura los encabezados del NDK para permitir referencias débiles. El segundo convierte la advertencia de llamadas a la API no seguras en un error.
Para obtener más información, consulta la guía para encargados de mantener sistemas de compilaciones.
Llamadas a la API protegidas
Esta función no hace que las llamadas a las APIs nuevas sean seguras por arte de magia. Lo único que hace es diferir un error de tiempo de carga a un error de tiempo de llamada. El beneficio es que puedes proteger esa llamada en el tiempo de ejecución y realizar una conmutación por error elegante, ya sea con una implementación alternativa o notificando al usuario que esa función de la app no está disponible en su dispositivo, o bien evitando esa ruta de código por completo.
Clang puede emitir una advertencia (unguarded-availability
) cuando realizas una llamada no protegida a una API que no está disponible para el minSdkVersion
de tu app. Si usas ndk-build o nuestro archivo de cadena de herramientas de CMake, esa advertencia se habilitará automáticamente y se convertirá en un error cuando habilites esta función.
A continuación, se muestra un ejemplo de código que hace uso condicional de una API sin esta función habilitada, con dlopen()
y 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);
}
}
Es un poco difícil de leer, hay cierta duplicación de nombres de función (y si escribes C, también las firmas), se compilará correctamente, pero siempre se usará el resguardo en el tiempo de ejecución si escribes accidentalmente el nombre de la función que se pasa a dlsym
, y debes usar este patrón para cada API.
Con referencias de API débiles, la función anterior se puede volver a escribir de la siguiente manera:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
De forma interna, __builtin_available(android 31, *)
llama a android_get_device_api_level()
, almacena en caché el resultado y lo compara con 31
(que es el nivel de API que introdujo AImageDecoder_resultToString()
).
La forma más sencilla de determinar qué valor usar para __builtin_available
es intentar compilar sin el resguardo (o un resguardo de __builtin_available(android 1, *)
) y hacer lo que te indica el mensaje de error.
Por ejemplo, una llamada no protegida a AImageDecoder_createFromAAsset()
con minSdkVersion 24
producirá lo siguiente:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
En este caso, __builtin_available(android 30, *)
debe proteger la llamada.
Si no hay un error de compilación, significa que la API siempre está disponible para tu minSdkVersion
y que no se necesita un resguardo, o bien que tu compilación está mal configurada y que la advertencia unguarded-availability
está inhabilitada.
Como alternativa, la referencia de la API del NDK dirá algo como "Presentada en la API 30" para cada API. Si no está presente, significa que la API está disponible para todos los niveles de API compatibles.
Evita la repetición de protecciones de API
Si usas esto, es probable que tengas secciones de código en tu app que solo se pueden usar en dispositivos lo suficientemente nuevos. En lugar de repetir la verificación de __builtin_available()
en cada una de tus funciones, puedes anotar tu propio código como que requiere un nivel de API determinado. Por ejemplo, las APIs de ImageDecoder se agregaron en la API 30, por lo que, para las funciones que usan mucho esas APIs, puedes hacer lo siguiente:
#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();
}
}
Particularidades de los protectores de API
Clang es muy particular en cuanto al uso de __builtin_available
. Solo funciona un if (__builtin_available(...))
literal (aunque posiblemente reemplazado por una macro). Ni siquiera las operaciones triviales como if (!__builtin_available(...))
funcionarán (Clang emitirá la advertencia unsupported-availability-guard
, así como unguarded-availability
). Esto puede mejorar en una versión futura de Clang. Consulta
el problema 33161 de LLVM para obtener más información.
Las verificaciones de unguarded-availability
solo se aplican al alcance de la función en la que se usan. Clang emitirá la advertencia incluso si a la función con la llamada a la API solo se le llama desde un alcance protegido. Para evitar la repetición de protecciones en
tu propio código, consulta Cómo evitar la repetición de protecciones de API.
¿Por qué no es la opción predeterminada?
A menos que se usen correctamente, la diferencia entre las referencias de API sólidas y las referencias de API débiles es que las primeras fallarán de forma rápida y obvia, mientras que las últimas no fallarán hasta que el usuario realice una acción que haga que se llame a la API faltante. Cuando esto suceda, el mensaje de error no será un error claro de tiempo de compilación "AFoo_bar() no está disponible", sino una falla de segmento. Con referencias sólidas, el mensaje de error es mucho más claro, y el error rápido es una configuración predeterminada más segura.
Como esta es una función nueva, se escribió muy poco código existente para controlar este comportamiento de forma segura. Es probable que el código de terceros que no se haya escrito teniendo en cuenta Android siempre tenga este problema, por lo que, por el momento, no hay planes para que el comportamiento predeterminado cambie.
Te recomendamos que uses esta opción, pero, como hará que sea más difícil detectar y depurar los problemas, debes aceptar esos riesgos de forma consciente en lugar de que el comportamiento cambie sin tu conocimiento.
Advertencias
Esta función funciona para la mayoría de las APIs, pero hay algunos casos en los que no funciona.
Las menos propensas a ser problemáticas son las APIs de libc más recientes. A diferencia del resto de las APIs de Android, estas están protegidas con #if __ANDROID_API__ >= X
en los encabezados y no solo con __INTRODUCED_IN(X)
, lo que evita que se vea incluso la declaración débil. Dado que el nivel de API más antiguo que admiten los NDK modernos es r21, las APIs de libc más comúnmente necesarias ya están disponibles. Se agregan nuevas APIs de libc con cada actualización (consulta status.md), pero cuanto más nuevas sean, es más probable que sean un caso extremo que pocos desarrolladores necesitarán. Dicho esto, si eres uno de esos desarrolladores, por ahora deberás seguir usando dlsym()
para llamar a esas APIs si tu minSdkVersion
es anterior a la API. Este es un problema que se puede resolver, pero hacerlo conlleva el riesgo de romper la compatibilidad de la fuente para todas las apps (cualquier código que contenga polyfills de las APIs de libc no se compilará debido a los atributos availability
no coincidentes en las declaraciones locales y de libc), por lo que no sabemos si lo solucionaremos ni cuándo.
El caso que es más probable que encuentren más desarrolladores es cuando la biblioteca que contiene la nueva API es más reciente que tu minSdkVersion
. Esta función solo habilita referencias de símbolos débiles; no existe una referencia de biblioteca débil. Por ejemplo, si tu minSdkVersion
es 24, puedes vincular libvulkan.so
y realizar una llamada protegida a vkBindBufferMemory2
, ya que libvulkan.so
está disponible en dispositivos a partir del nivel de API 24. Por otro lado, si tu minSdkVersion
fuera 23, debes recurrir a dlopen
y dlsym
, ya que la biblioteca no existirá en los dispositivos que solo admiten la API 23. No conocemos una buena solución para solucionar este caso, pero a largo plazo, se resolverá por sí solo porque, siempre que sea posible, ya no permitiremos que las nuevas APIs creen bibliotecas nuevas.
Para autores de bibliotecas
Si desarrollas una biblioteca para usarla en aplicaciones para Android, debes evitar usar esta función en tus encabezados públicos. Se puede usar de forma segura en el código fuera de línea, pero si dependes de __builtin_available
en cualquier código de tus encabezados, como funciones intercaladas o definiciones de plantillas, obligas a todos tus consumidores a habilitar esta función. Por los mismos motivos por los que no habilitamos esta función de forma predeterminada en el NDK, debes evitar tomar esa decisión en nombre de tus consumidores.
Si requieres este comportamiento en tus encabezados públicos, asegúrate de documentarlo para que los usuarios sepan que deberán habilitar la función y conozcan los riesgos de hacerlo.