Utilizzo di API più recenti

In questa pagina viene spiegato in che modo l'app può utilizzare le nuove funzionalità del sistema operativo quando viene eseguita su nuove versioni del sistema operativo, preservando la compatibilità con i dispositivi meno recenti.

Per impostazione predefinita, i riferimenti alle API NDK nella tua applicazione sono riferimenti efficaci. Il caricatore dinamico di Android provvederà a risolverli quando la libreria verrà caricata. Se i simboli non vengono trovati, l'app verrà interrotta. Ciò è contrario al comportamento di Java, in cui non viene generata un'eccezione fino a quando non viene chiamata l'API mancante.

Per questo motivo, l'NDK ti impedisce di creare riferimenti chiari ad API più recenti di minSdkVersion della tua app. In questo modo è possibile proteggerti dalla spedizione accidentale del codice che ha funzionato durante il test, ma che non riesce a caricare (UnsatisfiedLinkError verrà restituito da System.loadLibrary()) sui dispositivi più vecchi. D'altra parte, è più difficile scrivere codice che utilizzi API più recenti di minSdkVersion dell'app, perché devi chiamare le API utilizzando dlopen() e dlsym() anziché una normale chiamata di funzione.

L'alternativa all'utilizzo di riferimenti forti consiste nell'utilizzo di riferimenti deboli. Un riferimento debole che non viene trovato quando la libreria è stata caricata comporta l'impostazione dell'indirizzo di quel simbolo su nullptr anziché un errore di caricamento. Non è comunque possibile chiamarli in modo sicuro, ma se i siti di chiamata sono protetti per impedire di chiamare l'API quando non è disponibile, il resto del codice può essere eseguito e puoi chiamare l'API normalmente senza dover utilizzare dlopen() e dlsym().

I riferimenti API deboli non richiedono supporto aggiuntivo da parte del linker dinamico, quindi possono essere utilizzati con qualsiasi versione di Android.

Abilitazione dei riferimenti API deboli nella build

Marca

Supera -DANDROID_WEAK_API_DEFS=ON quando esegui CMake. Se utilizzi CMake tramite externalNativeBuild, aggiungi quanto segue a build.gradle.kts (o l'equivalente Groovy se utilizzi ancora build.gradle):

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

build-ndk

Aggiungi quanto segue al file Application.mk:

APP_WEAK_API_DEFS := true

Se non hai ancora un file Application.mk, crealo nella stessa directory del file Android.mk. Non sono necessarie ulteriori modifiche al file build.gradle.kts (o build.gradle) per ndk-build.

Altri sistemi di compilazione

Se non utilizzi CMake o ndk-build, consulta la documentazione del sistema di compilazione per scoprire se esiste un modo consigliato per abilitare questa funzionalità. Se il tuo sistema di compilazione non supporta questa opzione in modo nativo, puoi abilitare la funzionalità passando i seguenti flag durante la compilazione:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

La prima configura le intestazioni NDK per consentire riferimenti inefficaci. Il secondo trasforma in un errore l'avviso di chiamate API non sicure.

Per saperne di più, consulta la Guida per i gestori di sistema di Build.

Chiamate API protette

Questa funzionalità non effettua magicamente chiamate a nuove API in sicurezza. L'unica cosa che fa è rinviare un errore di tempo di caricamento a un errore di tempo di chiamata. Il vantaggio è che puoi proteggere quella chiamata in fase di runtime e ricorrere in modo controllato, utilizzando un'implementazione alternativa o avvisando l'utente che la funzionalità dell'app non è disponibile sul suo dispositivo oppure evitando del tutto il percorso del codice.

Clang può emettere un avviso (unguarded-availability) quando effettui una chiamata non protetta a un'API che non è disponibile per minSdkVersion della tua app. Se utilizzi ndk-build o il nostro file toolchain CMake, questo avviso verrà attivato automaticamente e segnalato come errore quando abiliti questa funzionalità.

Ecco un esempio di codice che esegue l'uso condizionale di un'API senza questa funzionalità abilitata, utilizzando dlopen() e 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);
    }
}

È un po' disordinato da leggere, i nomi delle funzioni sono duplicati (e se stai scrivendo C, anche le firme), la compilazione viene eseguita correttamente, ma viene sempre utilizzata la riserva in fase di runtime se digiti per errore il nome della funzione passato a dlsym e devi utilizzare questo pattern per ogni API.

Con riferimenti API deboli, la funzione precedente può essere riscritta come:

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

Dietro le quinte, __builtin_available(android 31, *) chiama android_get_device_api_level(), memorizza nella cache il risultato e lo confronta con 31 (che è il livello API che ha introdotto AImageDecoder_resultToString()).

Il modo più semplice per determinare quale valore utilizzare per __builtin_available è tentare di creare senza un sistema di protezione (o un sistema di protezione di __builtin_available(android 1, *)) e fare ciò che ti viene indicato dal messaggio di errore. Ad esempio, una chiamata non protetta a AImageDecoder_createFromAAsset() con minSdkVersion 24 produrrà:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

In questo caso la chiamata deve essere protetta da __builtin_available(android 30, *). Se non si verificano errori di generazione, l'API è sempre disponibile per minSdkVersion e non è necessaria alcuna protezione oppure la build non è configurata correttamente e l'avviso unguarded-availability è disabilitato.

In alternativa, il riferimento dell'API NDK riporterà una dicitura simile a "Introdotto nell'API 30" per ogni API. Se questo testo non è presente, significa che l'API è disponibile per tutti i livelli API supportati.

Evitare la ripetizione di protezioni API

Se la utilizzi, probabilmente avrai sezioni di codice nella tua app utilizzabili solo su dispositivi abbastanza nuovi. Anziché ripetere il controllo __builtin_available() in ciascuna funzione, puoi annotare il tuo codice in quanto richiede un determinato livello API. Ad esempio, le API ImageDecoder sono state aggiunte nell'API 30, quindi per le funzioni che fanno un uso intensivo di queste API puoi fare qualcosa come:

#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();
    }
}

stranezze di protezioni delle API

Clang descrive in modo molto particolare come viene utilizzato __builtin_available. Funziona solo un valore letterale (sebbene eventualmente sostituito da macro) if (__builtin_available(...)). Anche operazioni banali come if (!__builtin_available(...)) non funzioneranno (Clang emetterà l'avviso unsupported-availability-guard, così come unguarded-availability). Questo potrebbe migliorare in una versione futura di Clang. Per ulteriori informazioni, consulta il codice LLVM 33161.

I controlli per unguarded-availability si applicano solo all'ambito delle funzioni in cui vengono utilizzati. Clang emette l'avviso anche se la funzione con la chiamata API viene chiamata solo da un ambito protetto. Per evitare la ripetizione di protezioni API nel tuo codice, consulta Evitare la ripetizione di protezioni API.

Perché non è l'impostazione predefinita?

Se non vengono utilizzati correttamente, la differenza tra forti riferimenti API e riferimenti API deboli è che i primi hanno ovviamente esito negativo, mentre i secondi non avranno esito negativo finché l'utente non intraprende un'azione che causa la chiamata dell'API mancante. In questo caso, il messaggio di errore non consiste in un chiaro errore in fase di compilazione "AFoo_bar() non è disponibile", bensì di un segfault. Con riferimenti chiari, il messaggio di errore è molto più chiaro e un errore rapido è l'impostazione predefinita più sicura.

Trattandosi di una nuova funzionalità, è stato scritto molto poco codice esistente per gestire questo comportamento in sicurezza. Il codice di terze parti che non è stato scritto per Android probabilmente avrà sempre questo problema, quindi al momento non sono previsti cambiamenti per il comportamento predefinito.

Consigliamo di utilizzare questa funzionalità, ma poiché renderà i problemi più difficili da rilevare ed eseguire il debug, ti consigliamo di accettare questi rischi consapevolmente piuttosto che il cambiamento del comportamento a tua insaputa.

Precisazioni

Questa funzionalità funziona per la maggior parte delle API, ma in alcuni casi non funziona.

Le API libc più recenti sono la meno probabile. A differenza delle altre API Android, queste sono protette con #if __ANDROID_API__ >= X nelle intestazioni e non solo con __INTRODUCED_IN(X), il che impedisce di visualizzare anche la dichiarazione inefficace. Poiché il supporto più vecchio per gli NDK moderni a livello API è r21, sono già disponibili le API libc più comunemente necessarie. A ogni release vengono aggiunte nuove API libc (vedi status.md), ma più sono recenti, maggiori sono le probabilità che rappresentino un caso limite di cui pochi sviluppatori avranno bisogno. Detto questo, se sei uno di questi sviluppatori, per il momento dovrai continuare a utilizzare dlsym() per chiamare queste API se minSdkVersion è precedente all'API. Si tratta di un problema risolvibile, ma ciò comporta il rischio di interrompere la compatibilità dell'origine per tutte le app (qualsiasi codice contenente polyfill delle API libc non verrà compilato a causa di attributi availability non corrispondenti in libc e nelle dichiarazioni locali), quindi non siamo sicuri se o quando correggeremo il problema.

Un numero maggiore di sviluppatori probabilmente incontrerà quando la libreria contiene la nuova API è più recente di minSdkVersion. Questa funzionalità consente solo riferimenti di simboli deboli; non esiste un riferimento di libreria debole. Ad esempio, se il tuo minSdkVersion ha 24 anni, puoi collegare libvulkan.so ed effettuare una chiamata protetta al numero vkBindBufferMemory2, perché libvulkan.so è disponibile sui dispositivi che iniziano con l'API 24. Se invece minSdkVersion aveva 23, devi utilizzare dlopen e dlsym perché la libreria non esisterà sul dispositivo sui dispositivi che supportano solo l'API 23. Non conosciamo una soluzione valida per risolvere questo caso, ma nel lungo periodo si risolverà da solo perché (quando possibile) non permettiamo più a nuove API di creare nuove librerie.

Per autori di biblioteche

Se stai sviluppando una libreria da utilizzare nelle applicazioni Android, dovresti evitare di utilizzare questa funzionalità nelle intestazioni pubbliche. Può essere utilizzato in modo sicuro nel codice fuori riga, ma se utilizzi __builtin_available in qualsiasi codice nelle tue intestazioni, ad esempio funzioni incorporate o definizioni di modello, forzi tutti i tuoi consumer ad abilitare questa funzionalità. Per gli stessi motivi per cui non attiviamo questa funzionalità per impostazione predefinita nell'NDK, dovresti evitare di fare questa scelta per conto dei tuoi consumatori.

Se richiedi questo comportamento nelle intestazioni pubbliche, assicurati di documentarlo in modo che i tuoi utenti sappiano che dovranno abilitare la funzionalità e che siano consapevoli dei rischi che ciò comporta.