Questa pagina spiega in che modo la tua app può utilizzare le nuove funzionalità del sistema operativo quando viene eseguita su nuove versioni del sistema operativo, mantenendo al contempo la compatibilità con i dispositivi meno recenti.
Per impostazione predefinita, i riferimenti alle API NDK nella tua applicazione sono riferimenti forti. Il caricatore dinamico di Android cercherà di risolverli quando la raccolta viene caricata. Se i simboli non vengono trovati, l'app viene interrotta. Ciò è contrario al comportamento di Java, in cui un'eccezione non viene lanciata finché non viene chiamata l'API mancante.
Per questo motivo, l'NDK ti impedirà di creare riferimenti forti alle API più recenti del minSdkVersion
della tua app. In questo modo, eviterai di caricare accidentalmente codice che ha funzionato durante i test, ma che non verrà caricato (UnsatisfiedLinkError
verrà generato da System.loadLibrary()
) sui dispositivi meno recenti. 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 è l'utilizzo di riferimenti deboli. Un riferimento debole che non viene trovato quando la libreria viene caricata fa sì che l'indirizzo di quel simbolo venga impostato su nullptr
anziché non riuscire a caricarsi. Tuttavia, non possono essere chiamate in sicurezza, 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 ulteriore supporto dal linker dinamico, quindi possono essere utilizzati con qualsiasi versione di Android.
Abilitazione dei riferimenti API deboli nella build
CMake
Passa -DANDROID_WEAK_API_DEFS=ON
quando esegui CMake. Se utilizzi CMake tramite
externalNativeBuild
, aggiungi quanto segue al tuo build.gradle.kts
(o all'equivalente in Groovy se utilizzi ancora build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
Aggiungi quanto segue al tuo file Application.mk
:
APP_WEAK_API_DEFS := true
Se non hai già un file Application.mk
, creane uno nella stessa directory del file Android.mk
. Per ndk-build non sono necessarie modifiche aggiuntive al file build.gradle.kts
(o build.gradle
).
Altri sistemi di compilazione
Se non utilizzi CMake o ndk-build, consulta la documentazione del tuo sistema di compilazione per verificare se esiste un modo consigliato per attivare 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
Il primo configura le intestazioni NDK per consentire i riferimenti deboli. Il secondo trasforma l'avviso per le chiamate API non sicure in un errore.
Per ulteriori informazioni, consulta la Guida per i manutentori del sistema di compilazione.
Chiamate API protette
Questa funzionalità non rende magicamente sicure le chiamate alle nuove API. L'unica cosa che fa è rinviare un errore di tempo di caricamento a un errore di tempo di chiamata. Il vantaggio è che puoi difendere questa chiamata in fase di esecuzione e eseguire il fallback in modo corretto, ad esempio utilizzando un'implementazione alternativa o avvisando l'utente che la funzionalità dell'app non è disponibile sul suo dispositivo oppure evitando del tutto quel percorso di codice.
Clang può emettere un avviso (unguarded-availability
) quando effettui una chiamata non protetta a un'API non disponibile per il 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 utilizza in modo condizionale un'API senza attivare 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' complicato da leggere, c'è una certa duplicazione dei nomi delle funzioni (e se scrivi C, anche le firme), la compilazione andrà a buon fine, ma verrà sempre utilizzato il fallback in fase di esecuzione se sbagli accidentalmente a digitare il nome della funzione passato a dlsym
e devi utilizzare questo pattern per ogni API.
Con riferimenti API deboli, la funzione sopra 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, *)
chiamaandroid_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 eseguire la compilazione senza la guardia (o una guardia di __builtin_available(android 1, *)
) e seguire le istruzioni del 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 compilazione, l'API è sempre disponibile per il tuo
minSdkVersion
e non è necessaria alcuna guardia oppure la compilazione è configurata in modo errato e l'unguarded-availability
avviso è disattivato.
In alternativa, il riferimento all'API NDK conterrà una dicitura del tipo "Introdotta nell'API 30" per ogni API. Se il testo non è presente, significa che l'API è disponibile per tutti i livelli API supportati.
Evitare la ripetizione delle protezioni API
Se lo utilizzi, probabilmente la tua app conterrà sezioni di codice che possono essere utilizzate solo su dispositivi abbastanza recenti. Anziché ripetere il controllo__builtin_available()
in ogni funzione, puoi annotare il tuo codice come che richiede un determinato livello API. Ad esempio, le API ImageDecoder
stesse sono state aggiunte nell'API 30, quindi per le funzioni che fanno un uso intensivo di queste
API puoi fare qualcosa di simile:
#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();
}
}
Aspetti peculiari delle guardie API
Clang è molto esigente in merito all'utilizzo di __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
, nonché unguarded-availability
). Questo potrebbe migliorare in una versione futura di Clang. Per ulteriori informazioni, consulta
LLVM Issue 33161.
I controlli per unguarded-availability
si applicano solo all'ambito della funzione in cui vengono utilizzati. Clang emette l'avviso anche se la funzione con la chiamata API viene invocata solo da un ambito protetto. Per evitare la ripetizione delle guard nel
tuo codice, consulta Evitare la ripetizione delle guard API.
Perché non è l'impostazione predefinita?
A meno che non vengano utilizzati correttamente, la differenza tra i riferimenti API forti e i riferimenti API deboli è che i primi non andranno a buon fine in modo rapido e evidente, mentre i secondi non avranno esito negativo finché l'utente non eseguirà un'azione che causi la chiamata dell'API mancante. In questo caso, il messaggio di errore non sarà un chiaro errore di compilazione "AFoo_bar() non è disponibile", ma un errore di segfault. Con i riferimenti forti, il messaggio di errore è molto più chiaro e l'arresto anomalo rapido è un valore predefinito più sicuro.
Poiché si tratta di una nuova funzionalità, esiste pochissimo codice per gestire questo comportamento in modo sicuro. Il codice di terze parti che non è stato scritto pensando ad Android probabilmente avrà sempre questo problema, pertanto al momento non è prevista alcuna modifica al comportamento predefinito.
Ti consigliamo di utilizzare questa soluzione, 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 le meno probabili a causare problemi. A differenza del resto delle API Android, queste sono protette con #if __ANDROID_API__ >= X
negli intestazioni e non solo con __INTRODUCED_IN(X)
, il che impedisce anche la visualizzazione della dichiarazione debole. Poiché il livello API più antico supportato dagli NDK moderni è r21, le API libc più comunemente necessarie sono già disponibili. 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 nelle dichiarazioni locali e libc), quindi non siamo sicuri se o quando correggeremo il problema.
La situazione che più sviluppatori potrebbero riscontrare è quando la libreria che contiene la nuova API è più recente del tuo minSdkVersion
. Questa funzionalità consente solo di attivare i riferimenti a simboli deboli; non esiste un riferimento a librerie deboli. Ad esempio, se il tuo minSdkVersion
è 24, puoi collegare
libvulkan.so
ed eseguire una chiamata protetta a vkBindBufferMemory2
, perché
libvulkan.so
è disponibile sui dispositivi a partire dall'API 24. D'altra parte, se minSdkVersion
fosse 23, devi eseguire il fallback su dlopen
e dlsym
perché la raccolta non esisterà 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, devi evitare di utilizzare questa funzionalità negli header pubblici. Può essere utilizzato in modo sicuro nel codice out-of-line, ma se fai affidamento su __builtin_available
in qualsiasi codice degli intestazioni, ad esempio funzioni in linea o definizioni di modelli, forzi tutti i tuoi utenti a attivare 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 negli intestazioni pubblici, assicurati di documentarlo in modo che gli utenti sappiano che dovranno attivare la funzionalità e siano consapevoli dei rischi.