Usa APIs más recientes

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, mantiene la compatibilidad con dispositivos anteriores.

De forma predeterminada, las referencias a las APIs del NDK de tu aplicación son referencias sólidas. El cargador dinámico de Android los resolverá con anticipación cuando se cargue la biblioteca. Si no se encuentran los símbolos, se anulará 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 no te permitirá crear referencias sólidas a APIs más recientes que la minSdkVersion de tu app. De esta manera, evitas enviar accidentalmente 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 la 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 al uso de referencias fuertes es usar referencias débiles. Una referencia débil que no se encuentre cuando se cargue la biblioteca da como resultado que la dirección de ese símbolo se establezca en nullptr en lugar de que se produzca un error en la carga. No es posible llamarlas de forma segura, pero, siempre que los sitios de llamada estén protegidos para evitar llamar a la API cuando no está disponible, se podrá ejecutar el resto de tu código, y podrás llamar a la API normalmente sin necesidad de usar dlopen() ni dlsym().

Las referencias de API débiles no requieren compatibilidad adicional por parte del vinculador dinámico, por lo que se pueden usar con cualquier versión de Android.

Cómo habilitar referencias de la API poco seguras 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 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 se necesitan 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 existe 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. La segunda convierte en un error la advertencia sobre llamadas no seguras a la API.

Consulta la Guía para encargados de mantener sistemas de compilación a fin de obtener más información.

Llamadas a la API protegidas

Esta función no hace que las llamadas a APIs nuevas sean seguras. Lo único que hace es diferir un error de tiempo de carga a uno de tiempo de llamada. La ventaja es que puedes proteger esa llamada durante el tiempo de ejecución y regresar de forma correcta, ya sea usando una implementación alternativa o notificándole al usuario que esa función de la app no está disponible en su dispositivo, o evitando por completo esa ruta de código.

Clang puede emitir una advertencia (unguarded-availability) cuando realizas una llamada sin protección a una API que no está disponible para el elemento 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 mostrará como error cuando se habilite esta función.

A continuación, se muestra un ejemplo de un código que hace el 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);
    }
}

La lectura es un poco desordenada, hay algo de duplicación de los nombres de las funciones (y, si escribes C, 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 pasó a dlsym y debes usar este patrón para cada API.

Con referencias de API débiles, la función anterior se puede reescribir 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 la protección (o una protección de __builtin_available(android 1, *)) y hacer lo que te indica el mensaje de error. Por ejemplo, una llamada sin supervisión 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, la API siempre está disponible para tu minSdkVersion y no se necesita protección, o la compilación está mal configurada y se inhabilita la advertencia unguarded-availability.

De manera alternativa, la referencia de la API del NDK indicará algo similar a "Se introdujo en el nivel de API 30" para cada API. Si ese texto no está presente, significa que la API está disponible para todos los niveles de API admitidos.

Cómo evitar la repetición de protecciones de API

Si usas esto, es probable que tengas secciones de código en tu app que solo se puedan usar en dispositivos lo suficientemente nuevos. En lugar de repetir la verificación de __builtin_available() en cada una de tus funciones, puedes anotar que tu código requiere un determinado nivel de API. Por ejemplo, las APIs de ImageDecoder se agregaron en el nivel de API 30, por lo que para las funciones que hacen un uso intensivo de esas APIs puedes hacer algo como 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();
    }
}

Diferencias de los guardias de API

Clang es muy particular con el uso de __builtin_available. Solo funciona un if (__builtin_available(...)) literal (aunque posiblemente reemplazado por macros). Incluso las operaciones triviales, como if (!__builtin_available(...)), no 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 Error 33161 de LLVM para obtener más información.

Las verificaciones de unguarded-availability solo se aplican al alcance de la función en el que se usan. Clang emitirá la advertencia incluso si solo se llama a la función con la llamada a la API 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 use de forma correcta, la diferencia entre referencias a API sólidas y referencias débiles es que la primera fallará de manera rápida y obvia, mientras que la segunda no fallará hasta que el usuario realice una acción que provoque que se llame a la API faltante. Cuando esto sucede, el mensaje de error no será un error claro de tiempo de compilación "AFoo_bar() is not available", sino un error secundario. Con referencias sólidas, el mensaje de error es mucho más claro, y la falla rápida es un valor predeterminado más seguro.

Como se trata de una función nueva, se escribe muy poco código existente para manejar este comportamiento de forma segura. El código de terceros que no se escribió teniendo en cuenta a Android probablemente siempre tendrá este problema, por lo que, por el momento, no hay planes para que el comportamiento predeterminado cambie.

Te recomendamos que lo uses, pero como hará que los problemas sean más difíciles de detectar y depurar, debes aceptar esos riesgos a sabiendas, en lugar de cambiar el comportamiento sin que lo sepas.

Advertencias

Esta función funciona para la mayoría de las APIs, pero hay algunos casos en los que no.

La menos probable que haya problemas son las APIs de libc más nuevas. A diferencia del resto de las APIs de Android, esas 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. Como la compatibilidad más antigua de los NDK modernos con el nivel de API es r21, las APIs de libc más necesarias ya están disponibles. En cada versión se agregan nuevas APIs de libc (consulta status.md), pero cuanto más nuevas sean, más probable es que sean un caso límite 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 fallar la compatibilidad del código fuente para todas las apps (cualquier código que contenga polyfills de las APIs de libc no se compilará debido a los atributos availability que no coinciden en las declaraciones locales y de libc), por lo que no sabemos si lo solucionaremos o cuándo lo solucionaremos.

El caso que es probable que más desarrolladores encuentren es cuando la biblioteca que contiene la nueva API es más nueva que tu minSdkVersion. Esta función solo permite referencias de símbolos poco seguras; no existen referencias de biblioteca no seguras. Por ejemplo, si tu minSdkVersion es 24, puedes vincular libvulkan.so y realizar una llamada protegida a vkBindBufferMemory2, porque libvulkan.so está disponible en dispositivos a partir de la API 24. Por otro lado, si tu minSdkVersion fuera 23, debes recurrir a dlopen y dlsym porque la biblioteca no existirá en el dispositivo en dispositivos que solo admiten el nivel de API 23. No sabemos cuál es una buena solución para solucionar este caso, pero a largo plazo se resolverá sola porque (en la medida de lo posible) ya no permitimos que nuevas APIs creen bibliotecas nuevas.

Para autores de biblioteca

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 código fuera de línea, pero si confías en __builtin_available en cualquier código de tus encabezados, como funciones intercaladas o definiciones de plantillas, obligas a todos tus usuarios 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 tus usuarios sepan que necesitarán habilitar la función y que estén al tanto de los riesgos de hacerlo.