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 nuovi 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 sarà molto presto disponibile quando la tua raccolta caricato. Se i simboli non vengono trovati, l'app verrà interrotta. Ciò è contrario a il comportamento di Java, in cui non verrà generata un'eccezione fino a quando l'API mancante non verrà chiamato.

Per questo motivo, l'NDK ti impedisce di creare riferimenti forti API più recenti di minSdkVersion della tua app. Questo ti protegge spedizione accidentale di codice che ha funzionato durante i test ma che non viene caricato (UnsatisfiedLinkError verrà lanciata da System.loadLibrary()) su meno recenti dispositivi mobili. D'altra parte, è più difficile scrivere codice che utilizza le API più recente di minSdkVersion dell'app, perché devi chiamare le API utilizzando dlopen() e dlsym() al posto di una normale chiamata di funzione.

L'alternativa all'utilizzo di riferimenti forti consiste nell'utilizzo di riferimenti deboli. Un valore debole non trovato quando la libreria caricata restituisce l'indirizzo il simbolo viene impostato su nullptr anziché errori di caricamento. Ancora non può essere chiamato in modo sicuro, ma finché i siti di chiamata sono protetti per impedire le chiamate l'API quando non è disponibile, il resto del codice può essere eseguito chiama normalmente l'API senza dover usare dlopen() e dlsym().

I riferimenti API deboli non richiedono ulteriore supporto da parte del linker dinamico, per poter essere usati 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 Equivalente 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 già un file Application.mk, crealo nello stesso file come file Android.mk. Ulteriori modifiche a Il file build.gradle.kts (o build.gradle) non è necessario per ndk-build.

Altri sistemi di compilazione

Se non utilizzi CMake o ndk-build, consulta la documentazione della build per vedere se esiste un modo consigliato per attivare questa funzionalità. Se la tua build 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 turno l'avviso di chiamate API non sicure diventa un errore.

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 è rinviare un errore di tempo di caricamento a un errore di tempo di chiamata. Il vantaggio è che può proteggere la chiamata in fase di esecuzione e recuperare agevolmente, utilizzando una un'implementazione alternativa o informare l'utente che la funzionalità dell'app è non disponibile sul proprio dispositivo o di evitare del tutto il percorso del codice.

Clang può emettere un avviso (unguarded-availability) quando crei una chiamata incustodita a un'API non disponibile per minSdkVersion dell'app. Se utilizzando ndk-build o il nostro file toolchain CMake, l'avviso verrà automaticamente abilitato e promosso a un errore durante l'abilitazione di questa funzionalità.

Ecco un esempio di codice che esegue l'uso condizionale di un'API senza è stata attivata questa funzionalità, 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, ci sono dei duplicati di nomi di funzioni (e se stai scrivendo il C e anche le firme), si sviluppa in modo efficace, ma sempre utilizza la funzione di riserva in fase di runtime se digiti per errore il nome della funzione passato a dlsym e dovrai usare 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 (il livello API che ha introdotto AImageDecoder_resultToString()).

Il modo più semplice per determinare quale valore utilizzare per __builtin_available è tentare di costruire senza la sorveglianza (o una guardia __builtin_available(android 1, *)) e fai quanto ti viene mostrato il 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, significa che l'API è sempre disponibile minSdkVersion e non è necessaria alcuna protezione oppure la build non è configurata correttamente L'avviso unguarded-availability è disattivato.

In alternativa, il riferimento dell'API NDK indicherà qualcosa come "Introdotto nell'API 30" per ogni API. Se questo testo non è presente, significa che l'API sia disponibile per tutti i livelli API supportati.

Evitare la ripetizione di protezioni API

Se lo utilizzi, probabilmente avrai sezioni di codice nella tua app che possono essere usate solo su dispositivi abbastanza nuovi. Anziché ripetere __builtin_available() controlla ogni funzione, puoi annotare le tue il proprio codice poiché richiede un determinato livello API. Ad esempio, le API ImageDecoder sono stati aggiunti nell'API 30, quindi per le funzioni che fanno un uso intensivo 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. Solo un valore letterale (anche se potrebbe essere sostituito in modo macroscopico) if (__builtin_available(...)) funziona. Uniforme le operazioni basilari come if (!__builtin_available(...)) non funzioneranno (Clang emetterà l'avviso unsupported-availability-guard, oltre a unguarded-availability). Questa funzionalità potrebbe migliorare in una versione futura di Clang. Consulta Per ulteriori informazioni, consulta il codice LLVM 33161.

I controlli per unguarded-availability si applicano solo all'ambito delle funzioni in cui . Clang emetterà l'avviso anche se la funzione con la chiamata API viene chiamate solo da un ambito protetto. Per evitare che le guardie si ripetano l'inserimento del codice, consulta Evitare la ripetizione di protezioni API.

Perché non è l'impostazione predefinita?

Se non viene utilizzato correttamente, la differenza tra forti riferimenti API e un'API debole è che la prima fallisce rapidamente e ovviamente, mentre la seconda non fallirà finché l'utente non intraprenderà un'azione che causa la mancanza dell'API per essere chiamati. In questi casi, il messaggio di errore non sarà chiaro. compilazione: "AFoo_bar() non è disponibile" si tratta di un segfault. Con riferimenti chiari, il messaggio di errore è molto più chiaro e un errore è un predefinita più sicura.

Trattandosi di una nuova funzionalità, è stato scritto molto poco codice esistente per gestire questo comportamento in modo sicuro. Codice di terze parti non scritto per Android probabilmente avrà sempre questo problema, quindi al momento non sono previsti piani per il comportamento predefinito possa cambiare in continuazione.

Ti consigliamo di utilizzare questa funzionalità, ma poiché i problemi saranno sempre di più difficili da rilevare ed eseguire il debug, è consigliabile accettare questi rischi consapevolmente, del cambiamento di comportamento a tua insaputa.

Precisazioni

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

Le API libc più recenti sono la meno probabile. A differenza degli altri API Android, protette da #if __ANDROID_API__ >= X nelle intestazioni e non solo __INTRODUCED_IN(X), il che impedisce anche alla dichiarazione inefficace di essere visti. Dal momento che il supporto più obsoleto per gli NDK moderni a livello API è r21, le API libc comunemente necessarie sono già disponibili. Vengono aggiunte nuove API libc ogni (vedi status.md), ma più sono recenti, maggiori sono le probabilità che un caso limite di cui pochi sviluppatori avranno bisogno. Detto questo, se sei uno questi sviluppatori, per il momento dovrai continuare a usare dlsym() per chiamare API se minSdkVersion è precedente all'API. È un problema risolvibile, ma comporta il rischio di interrompere la compatibilità della fonte per tutte le app (qualsiasi il codice che contiene polyfill delle API libc non verrà compilato a causa attributi availability non corrispondenti nelle dichiarazioni libc e locali), quindi non siamo sicuri se o quando correggeremo il problema.

Il caso che più sviluppatori probabilmente incontreranno è quando la libreria che contiene la nuova API sia più recente di minSdkVersion. Solo questa funzionalità consente riferimenti di simboli inefficaci; non esiste una libreria debole riferimento. Ad esempio, se il tuo minSdkVersion ha 24 anni, puoi collegare libvulkan.so ed effettua una chiamata con guardia riservata a vkBindBufferMemory2, perché libvulkan.so è disponibile sui dispositivi a partire dall'API 24. D'altra parte, se minSdkVersion aveva 23, devi tornare a dlopen e dlsym perché la libreria non esisterà sul dispositivo sui dispositivi che supportano solo API 23. Non conosciamo una soluzione valida per risolvere questo problema, ma nel lungo termine si risolverà da solo perché (laddove possibile) non consentiamo più API per creare nuove librerie.

Per autori di biblioteche

Se stai sviluppando una libreria da utilizzare nelle app Android, devi evita di usare questa funzione nelle intestazioni pubbliche. Può essere utilizzato in sicurezza fuori riga, ma se utilizzi __builtin_available in qualsiasi codice nella tua come le funzioni in linea o le definizioni di modelli, forzi per abilitare questa funzionalità. Per gli stessi motivi per cui non attiviamo funzione per impostazione predefinita nell'NDK, dovresti evitare di fare questa scelta per conto dei tuoi consumatori.

Se hai bisogno di questo comportamento nelle tue intestazioni pubbliche, assicurati di documentare in modo che gli utenti sappiano che devono attivare la funzionalità consapevoli dei rischi che questa scelta comporta.