JNI est l'interface native de Java. Elle définit un moyen pour le bytecode qu'Android compile à partir de code géré (écrit dans les langages de programmation Java ou Kotlin) d'interagir avec le code natif (écrit en C/C++). JNI est indépendante des fournisseurs, prend en charge le chargement de code à partir de bibliothèques partagées dynamiques et, bien que parfois lourde, est raisonnablement efficace.
Remarque:Android compile Kotlin en bytecode compatible avec ART dans de la même manière que pour le langage de programmation Java, vous pouvez appliquer les conseils fournis sur cette page les langages de programmation Kotlin et Java en termes d'architecture JNI et de coûts associés. Pour en savoir plus, consultez Kotlin et Android.
Si vous ne la connaissez pas encore, consultez la spécification de l'interface native Java pour comprendre le fonctionnement de JNI et les fonctionnalités disponibles. Un peu certains aspects de l'interface ne sont pas immédiatement évidents sur première lecture. Vous trouverez donc peut-être les sections suivantes à portée de main.
Pour parcourir les références JNI globales et voir où elles sont créées et supprimées, utilisez la vue Tas de mémoire JNI dans le Profileur de mémoire d'Android Studio 3.2 ou version ultérieure.
Conseils généraux
Essayez de réduire l'empreinte de votre couche JNI. Plusieurs dimensions sont à prendre en compte. Votre solution JNI doit essayer de suivre ces consignes (répertoriées ci-dessous par ordre d'importance, en commençant par le plus important):
- Minimisez le marshaling des ressources dans la couche JNI. Le marshalling au niveau de la couche JNI entraîne des coûts non négligeables. Essayez de concevoir une interface qui minimise la quantité de données que vous devez marshaller et la fréquence à laquelle vous devez le faire.
- Évitez autant que possible la communication asynchrone entre le code écrit dans un langage de programmation géré et le code écrit en C++. Cela facilitera la gestion de votre interface JNI. Vous pouvez généralement simplifier les mises à jour asynchrones de l'UI en conservant la mise à jour asynchrone dans la même langue que l'UI. Par exemple, au lieu de appeler une fonction C++ à partir du thread UI dans le code Java via JNI, il est préférable pour effectuer un rappel entre deux threads dans le langage de programmation Java, avec l'un d'entre eux effectuer un appel C++ bloquant, puis avertir le thread UI lorsque l'appel bloquant est terminé.
- Réduisez le nombre de threads qui doivent toucher ou être touchés par JNI. Si vous devez utiliser des pools de threads dans les langages Java et C++, essayez de conserver JNI entre les propriétaires du pool plutôt qu'entre les threads de nœuds de calcul individuels.
- Conservez le code de votre interface dans un faible nombre de sources C++ et Java facilement identifiables. emplacements pour faciliter les futures refactorisations. Envisagez d'utiliser une instance de génération automatique JNI bibliothèque, le cas échéant.
JavaVM et JNIEnv
JNI définit deux structures de données clés : "JavaVM" et « JNIEnv ». Ces deux exemples sont essentiellement des pointeurs vers des tableaux de fonctions. (Dans la version C++, ce sont des classes avec pointeur vers une table de fonctions et une fonction membre pour chaque fonction JNI indirecte via le tableau.) Le JavaVM fournit les fonctions d'interface d'appel, qui vous permettent de créer et de détruire un JavaVM. En théorie, vous pouvez avoir plusieurs VM Java par processus, mais Android n'en autorise qu'une.
JNIEnv fournit la plupart des fonctions JNI. Vos fonctions natives reçoivent toutes un JNIEnv comme premier argument, à l'exception des méthodes @CriticalNative
. Consultez la section Appels natifs plus rapides.
JNIEnv est utilisé pour le stockage thread local. Par conséquent, vous ne pouvez pas partager un JNIEnv entre les threads.
Si un extrait de code ne dispose d'aucun autre moyen d'obtenir son JNIEnv, vous devez partager
JavaVM et utiliser GetEnv
pour découvrir le JNIEnv du thread. (en supposant qu'il en existe un, voir AttachCurrentThread
ci-dessous).
Les déclarations C de JNIEnv et JavaVM sont différentes des déclarations C++. Le fichier d'inclusion "jni.h"
fournit différentes définitions de type.
selon qu’il est inclus en C ou C++. Pour cette raison,
il est déconseillé de
inclure des arguments JNIEnv dans les fichiers d'en-tête inclus dans les deux langages. (En d'autres termes, si votre
le fichier d'en-tête nécessite #ifdef __cplusplus
, vous devrez peut-être effectuer des opérations supplémentaires dans
cet en-tête fait référence à JNIEnv.)
Threads
Tous les threads sont des threads Linux, programmés par le noyau. Ils sont généralement
démarré à partir de code géré (avec Thread.start()
) ;
mais elles peuvent également être créées ailleurs, puis associées à JavaVM
. Pour
exemple, un fil de discussion commencé par pthread_create()
ou std::thread
peut être associé à l'aide de l'AttachCurrentThread()
ou
Fonctions AttachCurrentThreadAsDaemon()
. Jusqu'à ce qu'un fil de discussion
n'a pas de JNIEnv et ne peut pas effectuer d'appels JNI.
Il est généralement préférable d'utiliser Thread.start()
pour créer tous les threads qui doivent
au code Java. Vous vous assurez ainsi de disposer d'un espace de pile suffisant, d'être dans le ThreadGroup
approprié et d'utiliser le même ClassLoader
que votre code Java. Il est également plus facile de définir le nom du thread pour le débogage en Java que depuis le code natif (voir pthread_setname_np()
si vous disposez d'un pthread_t
ou d'un thread_t
, et std::thread::native_handle()
si vous disposez d'un std::thread
et que vous souhaitez un pthread_t
).
L'association d'un thread créé en mode natif entraîne la création et l'ajout d'un objet java.lang.Thread
au ThreadGroup
"principal", ce qui le rend visible pour le débogueur. Vous appelez AttachCurrentThread()
...
sur un thread déjà associé est une no-op.
Android ne suspend pas les threads qui exécutent du code natif. Si la récupération de mémoire est en cours ou le débogueur a émis une requête requête, Android met en pause le thread lors du prochain appel JNI.
Les threads associés via JNI doivent appeler DetachCurrentThread()
avant de se terminer.
Si le codage direct est gênant, dans Android 2.0 (Eclair) et versions ultérieures,
Vous pouvez utiliser pthread_key_create()
pour définir un destructeur
qui sera appelée avant la fermeture du thread, et
puis appelez DetachCurrentThread()
. (Utilisez ce
avec pthread_setspecific()
pour stocker JNIEnv
thread-local-storage. afin qu'elle soit transmise à votre destructeur
l'argument.)
jclass, jmethodID et jfieldID
Pour accéder au champ d'un objet à partir de code natif, procédez comme suit :
- Obtenir la référence de l'objet de classe pour la classe avec
FindClass
- Obtenez l'ID du champ avec
GetFieldID
. - Récupérez le contenu du champ à l'aide d'un élément approprié, tel que
GetIntField
De même, pour appeler une méthode, vous devez d'abord obtenir une référence d'objet de classe, puis un ID de méthode. Les ID sont souvent de simples des pointeurs vers des structures de données d'exécution interne. La recherche peut nécessiter plusieurs comparaisons de chaînes, mais une fois que vous les avez, l'appel réel pour obtenir le champ ou appeler la méthode est très rapide.
Si les performances sont importantes, il est utile de rechercher les valeurs une fois et de mettre les résultats en cache dans votre code natif. Étant donné qu'il existe une limite d'une VM Java par processus, il est raisonnable pour stocker ces données dans une structure locale statique.
Les références de classe, les ID de champ et les ID de méthode sont garantis valides jusqu'à ce que la classe soit désinstallée. Classes
ne sont déchargés que si toutes les classes associées à un ClassLoader peuvent être récupérées,
ce qui est rare, mais qui ne
sera pas impossible sur Android. Notez toutefois que jclass
est une référence de classe et doit être protégée par un appel à NewGlobalRef
(voir la section suivante).
Si vous souhaitez mettre en cache les ID lorsqu'une classe est chargée et les remettre automatiquement en cache si la classe est déchargée et rechargée, la bonne façon d'initialiser les ID consiste à ajouter un extrait de code semblable à celui-ci à la classe appropriée :
Kotlin
companion object { /* * We use a static class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private external fun nativeInit() init { nativeInit() } }
Java
/* * We use a class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private static native void nativeInit(); static { nativeInit(); }
Dans votre code C/C++, créez une méthode nativeClassInit
qui effectue les recherches d'ID. Le code
est exécutée une fois, lorsque la classe est initialisée. Si la classe est déchargée, puis rechargée, elle sera à nouveau exécutée.
Références locales et internationales
Chaque argument transmis à une méthode native, et presque tous les objets renvoyés par une fonction JNI est une "référence locale". Cela signifie qu'il est valide pour la durée de la méthode native actuelle dans le thread actuel. Même si l'objet lui-même continue de vivre après la méthode native la référence n'est pas valide.
Cela s'applique à toutes les sous-classes de jobject
, y compris jclass
, jstring
et jarray
.
(L'environnement d'exécution vous avertira de la plupart des utilisations abusives des références lorsque les vérifications JNI étendues sont activées.)
Le seul moyen d'obtenir des références non locales est d'utiliser les fonctions
NewGlobalRef
et NewWeakGlobalRef
.
Si vous souhaitez conserver une référence plus longtemps, vous devez utiliser une référence "globale". La fonction NewGlobalRef
utilise
référence locale en tant qu'argument et renvoie une référence globale.
La validité de la référence globale est garantie jusqu'à ce que vous l'appeliez
DeleteGlobalRef
Ce modèle est couramment utilisé lors de la mise en cache d'une classe jclass renvoyée
de FindClass
. Exemple :
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Toutes les méthodes JNI acceptent des références locales et globales comme arguments.
Il est possible que les références à un même objet aient des valeurs différentes.
Par exemple, les valeurs renvoyées par des appels consécutifs à
NewGlobalRef
sur le même objet peut être différent.
Pour savoir si deux références font référence au même objet, vous devez utiliser la fonction IsSameObject
. Ne comparez jamais des références avec ==
dans le code natif.
Par conséquent, vous ne devez pas supposer que les références d'objets sont constantes ou uniques dans le code natif. La valeur représentant un objet peut être différente d'une invocation de méthode à l'autre, et il est possible que deux objets différents aient la même valeur lors d'appels consécutifs. Ne pas utiliser
jobject
comme clés.
Les programmeurs ne doivent pas "allouer trop de ressources" aux références locales. Concrètement, cela signifie
que si vous créez un grand nombre de références locales, peut-être en parcourant un tableau
vous devez les libérer manuellement
DeleteLocalRef
au lieu de laisser JNI le faire pour vous. La
n'est requise que pour réserver des emplacements
16 références locales. Si vous avez besoin d'un plus grand nombre de références locales, vous devez les supprimer au fur et à mesure ou utiliser
EnsureLocalCapacity
/PushLocalFrame
pour en réserver davantage.
Notez que les éléments jfieldID
et jmethodID
sont opaques.
et non de références d'objets, et ne doit pas être transmise
NewGlobalRef
Les données brutes
Les pointeurs renvoyés par des fonctions telles que GetStringUTFChars
et GetByteArrayElements
ne sont pas non plus des objets. (Ils peuvent être transmis
entre les threads et sont valides jusqu'à l'appel de lancement correspondant.)
Un cas inhabituel mérite d'être mentionné. Si vous associez un fichier natif
thread avec AttachCurrentThread
, le code que vous exécutez
ne libère jamais automatiquement les références locales tant que le thread n'est pas détaché. N'importe quelle région
les références que vous créez
doivent être supprimées manuellement. En règle générale, tout code natif qui crée des références locales dans une boucle doit probablement effectuer une suppression manuelle.
Faites attention à utiliser des références globales. Les références globales sont inévitables, mais elles sont difficiles à déboguer et peut entraîner des problèmes de diagnostic difficiles à diagnostiquer. Toutes choses égales par ailleurs, un une solution avec moins de références globales est probablement préférable.
Chaînes UTF-8 et UTF-16
Le langage de programmation Java utilise l'encodage UTF-16. Pour des raisons pratiques, JNI fournit également des méthodes compatibles avec l'UTF-8 modifié. La L'encodage modifié est utile pour le code C, car il encode \u0000 en 0xc0 0x80 au lieu de 0x00. L'avantage est que vous pouvez compter sur des chaînes terminées par zéro de style C, adaptées à l'utilisation avec les fonctions de chaîne libc standards. L'inconvénient est que vous ne pouvez pas transmettre des données UTF-8 arbitraires à JNI et vous attendre à ce qu'elles fonctionnent correctement.
Pour obtenir la représentation UTF-16 d'un String
, utilisez GetStringChars
.
Notez que les chaînes UTF-16 ne se terminent pas par zéro et que \u0000 est autorisé. Vous devez donc conserver la longueur de la chaîne ainsi que le pointeur jchar.
N'oubliez pas d'utiliser Release
les chaînes que vous Get
. La
les fonctions de chaîne renvoient jchar*
ou jbyte*
, qui
sont des pointeurs de style C vers des données primitives plutôt que vers des références locales. Ils
sont valides jusqu'à l'appel de la méthode Release
, ce qui signifie qu'elles ne sont pas
et libéré lorsque la méthode native renvoie un résultat.
Les données transmises à NewStringUTF doivent être au format UTF-8 modifié. A
erreur courante consiste à lire les données de caractères à partir d'un fichier ou d'un flux réseau
et le transmettre à NewStringUTF
sans le filtrer.
À moins que vous ne sachiez que les données sont valides en MUTF-8 (ou ASCII 7 bits, qui est un sous-ensemble compatible),
vous devez supprimer les caractères non valides ou
les convertir en format UTF-8 modifié correct.
Si vous ne le faites pas, la conversion UTF-16 risque de produire des résultats inattendus.
CheckJNI, qui est activé par défaut pour les émulateurs, analyse les chaînes
et abandonne la VM si elle reçoit une entrée non valide.
Avant Android 8, il était généralement plus rapide d'utiliser des chaînes UTF-16 qu'Android
ne nécessitait pas de copie dans GetStringChars
, alors que
GetStringUTFChars
a nécessité une allocation et une conversion au format UTF-8.
Android 8 a modifié la représentation String
pour utiliser 8 bits par caractère
pour les chaînes ASCII (pour économiser de la mémoire) et nous avons commencé à utiliser
en mouvement
le récupérateur de mémoire. Ces fonctionnalités réduisent considérablement le nombre de cas où ART
peut fournir un pointeur vers les données String
sans créer de copie, même
pour GetStringCritical
. Toutefois, si la plupart des chaînes traitées par le code sont courtes, il est possible d'éviter l'allocation et la désallocation dans la plupart des cas en utilisant un tampon alloué sur la pile et GetStringRegion
ou GetStringUTFRegion
. Exemple :
constexpr size_t kStackBufferSize = 64u; jchar stack_buffer[kStackBufferSize]; std::unique_ptr<jchar[]> heap_buffer; jchar* buffer = stack_buffer; jsize length = env->GetStringLength(str); if (length > kStackBufferSize) { heap_buffer.reset(new jchar[length]); buffer = heap_buffer.get(); } env->GetStringRegion(str, 0, length, buffer); process_data(buffer, length);
Tableaux primitifs
JNI fournit des fonctions permettant d'accéder au contenu des objets d'un tableau. Bien qu'il soit nécessaire d'accéder aux tableaux d'objets une entrée à la fois, les tableaux de les primitives peuvent être lues et écrites directement comme si elles étaient déclarées en C.
Rendre l'interface aussi efficace que possible sans contraintes
l'implémentation de la VM, le Get<PrimitiveType>ArrayElements
d'appels permet à l'environnement d'exécution de renvoyer un pointeur vers les éléments réels
d’allouer de la mémoire et
d’en faire une copie. Dans tous les cas, le pointeur brut a renvoyé
d'être valide jusqu'à l'appel Release
correspondant
(ce qui implique que, si les données n'ont pas été copiées, l'objet de tableau
sera épinglée et ne pourra pas être déplacée dans le cadre du compactage du tas de mémoire).
Vous devez utiliser Release
pour chaque tableau que vous Get
. De plus, si Get
l'appel échoue, vous devez vous assurer que votre code ne tente pas de Release
une valeur NULL
.
Vous pouvez déterminer si les données ont été copiées en transmettant un pointeur non NULL pour l'argument isCopy
. C'est rarement le cas
utiles.
L'appel Release
utilise un argument mode
qui peut
ont l'une des trois valeurs suivantes : Les actions effectuées par l'environnement d'exécution dépendent
s'il a renvoyé un pointeur vers les données réelles ou une copie de celles-ci:
0
- Réel : l'objet tableau n'est plus épinglé.
- Copier: les données sont recopiées. Le tampon contenant la copie est libéré.
JNI_COMMIT
- Réel : ne fait rien.
- Copier: les données sont recopiées. Tampon avec la copie n'est pas libérée.
JNI_ABORT
- Réel: l'objet de tableau n'est plus épinglé. Avant les écritures ne sont pas annulées.
- Copy: le tampon contenant la copie est libéré. toutes les modifications apportées sont perdues.
L'une des raisons de vérifier l'indicateur isCopy
est de savoir si vous devez appeler Release
avec JNI_COMMIT
après avoir modifié un tableau. Si vous alternez entre les modifications et l'exécution de code qui utilise le contenu du tableau, vous pouvez peut-être ignorer le commit sans opération. Une autre raison possible pour
vérifier l'indicateur est que
gestion efficace de JNI_ABORT
. Par exemple, vous pouvez obtenir un tableau, le modifier à son emplacement, transmettre des éléments à d'autres fonctions, puis supprimer les modifications. Si vous savez que JNI crée une nouvelle copie pour vous, vous n'avez pas besoin de créer une autre copie "modifiable". Si JNI transmet
vous l'original, alors vous devez
faire votre propre copie.
Une erreur courante (répétée dans l'exemple de code) consiste à supposer que vous pouvez ignorer l'appel Release
si
*isCopy
est "false". Ce n'est pas le cas. Si aucun tampon de copie n'a été alloué, la mémoire d'origine doit être épinglée et ne peut pas être déplacée par le garbage collector.
Notez également que l'option JNI_COMMIT
ne libère pas le tableau.
et vous devrez à nouveau appeler Release
avec un autre indicateur.
à terme.
Appels par région
Il existe une alternative aux appels comme Get<Type>ArrayElements
et GetStringChars
, qui peuvent être très utiles
consiste à copier des données
vers ou en dehors. Réfléchissez aux points suivants :
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
Cette opération récupère le tableau, en copie les premiers éléments d'octet len
, puis libère le tableau. Selon l'implémentation, l'appel Get
épinglera ou copiera le contenu du tableau.
Le code copie les données (peut-être une deuxième fois), puis appelle Release
. Dans ce cas, JNI_ABORT
garantit qu'il n'y a aucune chance d'une troisième copie.
On peut accomplir la même chose plus simplement:
env->GetByteArrayRegion(array, 0, len, buffer);
Ce fonctionnement offre plusieurs avantages :
- Nécessite un appel JNI au lieu de deux, ce qui réduit les frais généraux.
- Ne nécessite pas d'épinglage ni de copie de données supplémentaire.
- Réduit le risque d'erreur du programmeur, sans risque d'oublier
pour appeler
Release
en cas d'échec.
De même, vous pouvez utiliser l'appel Set<Type>ArrayRegion
.
pour copier des données dans un tableau, et GetStringRegion
ou
GetStringUTFRegion
pour copier des caractères à partir d'un
String
Exceptions
Vous ne devez pas appeler la plupart des fonctions JNI tant qu'une exception est en attente.
Votre code doit remarquer l'exception (via la valeur de retour de la fonction, ExceptionCheck
ou ExceptionOccurred
) et renvoyer, ou effacer l'exception et la gérer.
Les seules fonctions JNI que vous êtes autorisé à appeler, alors qu'une exception est en attente:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
De nombreux appels JNI peuvent générer une exception, mais fournissent souvent un moyen plus simple
de la vérification des défaillances. Par exemple, si NewString
renvoie
une valeur non nulle, vous n'avez pas besoin de rechercher une exception. Toutefois, si vous appelez une méthode (à l'aide d'une fonction telle que CallObjectMethod
), vous devez toujours vérifier la présence d'une exception, car la valeur de retour ne sera pas valide si une exception a été générée.
Notez que les exceptions générées par du code géré ne déroulent pas la pile native.
cadres. Notez que les exceptions C++, généralement déconseillées sur Android, ne doivent pas
générée au-delà de la limite de transition JNI, du code C++ au code géré.)
Les instructions JNI Throw
et ThrowNew
juste
définir un pointeur d'exception dans le thread actuel. Lors du retour sur la page gérée
du code natif, l'exception sera notée et gérée de manière appropriée.
Le code natif peut "intercepter" une exception en appelant ExceptionCheck
ou ExceptionOccurred
, et la vider avec ExceptionClear
. Comme d'habitude,
le rejet des exceptions sans les traiter
peut entraîner des problèmes.
Aucune fonction intégrée n'est disponible pour manipuler l'objet Throwable
elle-même. Ainsi, si vous voulez (par exemple) obtenir la chaîne d'exception, vous devez
recherchez la classe Throwable
, recherchez l'ID de la méthode
getMessage "()Ljava/lang/String;"
, appelez-le et, si le résultat
est non NULL. Utilisez GetStringUTFChars
pour obtenir une valeur
à printf(3)
ou équivalent.
Vérification étendue
JNI effectue très peu de vérifications d'erreur. Les erreurs entraînent généralement un plantage. Android propose également un mode appelé CheckJNI, dans lequel les pointeurs de table des fonctions JavaVM et JNIEnv sont basculés vers des tables de fonctions qui effectuent une série étendue de vérifications avant d'appeler l'implémentation standard.
Les vérifications supplémentaires incluent les éléments suivants:
- Tableaux: tentative d'allocation d'un tableau de taille négative.
- Pointeurs incorrects: transmission d'un mauvais jarray/jclass/jobject/jstring à un appel JNI, ou transmission d'un pointeur NULL à un appel JNI avec un argument non nullable.
- Noms de classe: transmission de tout nom de classe autre que le style "java/lang/String" à un appel JNI.
- Appels critiques : effectuez un appel JNI entre une récupération "critique" et sa version correspondante.
- Direct ByteBuffers: transmission des arguments incorrects à
NewDirectByteBuffer
. - Exceptions: effectuer un appel JNI alors qu'une exception est en attente.
- JNIEnv*s: utilisation d'un JNIEnv* provenant du mauvais thread.
- jfieldIDs: utilisation d'un jfieldID NULL ou utilisation d'un jfieldID pour définir un champ sur une valeur d'un type incorrect (essayer d'attribuer un StringBuilder à un champ String, par exemple), utiliser un jfieldID pour un champ statique pour définir un champ d'instance ou inversement, ou utiliser un jfieldID d'une classe avec des instances d'une autre classe.
- jmethodIDs: utilisation du mauvais genre de jmethodID lors d'un appel JNI
Call*Method
: type renvoyé incorrect, non-concordance statique/non statique, type incorrect pour "this" (pour les appels non statiques) ou mauvaise classe (pour les appels statiques). - Références: utilisation de
DeleteGlobalRef
/DeleteLocalRef
sur le mauvais type de référence. - Modes de lancement: transmission d'un mode de publication incorrect à un appel de version (autre que
0
,JNI_ABORT
ouJNI_COMMIT
). - Sécurité du typage : renvoiez un type incompatible à partir de votre méthode native (par exemple, renvoyez un StringBuilder à partir d'une méthode déclarée pour renvoyer une chaîne).
- UTF-8 : transmission d'une séquence d'octets UTF-8 modifié non valide à un appel JNI.
(L'accessibilité des méthodes et des champs n'est toujours pas vérifiée : les restrictions d'accès ne s'appliquent pas au code natif.)
Il existe plusieurs façons d'activer CheckJNI.
Si vous utilisez l'émulateur, CheckJNI est activé par défaut.
Si vous possédez un appareil en mode root, vous pouvez utiliser la séquence de commandes suivante pour redémarrer l'environnement d'exécution avec CheckJNI activé :
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
Dans les deux cas, la sortie logcat doit ressembler à ceci au démarrage de l'environnement d'exécution:
D AndroidRuntime: CheckJNI is ON
Si vous disposez d'un appareil standard, vous pouvez utiliser la commande suivante:
adb shell setprop debug.checkjni 1
Cela n'affectera pas les applications déjà en cours d'exécution, mais CheckJNI sera activé pour toutes les applications lancées à partir de ce moment-là. (Remplacez la propriété par une autre valeur ou redémarrez simplement CheckJNI.) Dans ce cas, la sortie logcat devrait ressembler à ceci au prochain démarrage d'une application:
D Late-enabling CheckJNI
Vous pouvez également définir l'attribut android:debuggable
dans le fichier manifeste de votre application pour activer CheckJNI uniquement pour votre application. Notez que les outils de compilation Android le feront automatiquement pour certains types de compilation.
Bibliothèques natives
Vous pouvez charger du code natif à partir de bibliothèques partagées avec System.loadLibrary
standard.
En pratique, les anciennes versions d'Android comportaient des bugs dans PackageManager qui affectaient la fiabilité de l'installation et de la mise à jour des bibliothèques natives. ReLinker propose des solutions de contournement à ce problème et à d'autres problèmes de chargement de bibliothèques natives.
Appeler System.loadLibrary
(ou ReLinker.loadLibrary
) à partir d'une classe statique
initialiseur. L'argument est le nom de la bibliothèque "non décorée". Pour charger libfubar.so
, vous devez transmettre "fubar"
.
Si vous n'avez qu'une seule classe avec des méthodes natives, il est logique que l'appel de System.loadLibrary
se trouve dans un initialiseur statique pour cette classe. Sinon, vous pouvez effectuer l'appel à partir de Application
pour savoir que la bibliothèque est toujours chargée et toujours chargée à l'avance.
L'environnement d'exécution peut trouver vos méthodes natives de deux manières. Vous pouvez soit explicitement
les enregistrer avec RegisterNatives
, ou laisser l'environnement d'exécution les rechercher dynamiquement
avec dlsym
. Avec RegisterNatives
, vous bénéficiez d'une visibilité initiale
en vérifiant que les symboles existent. De plus, vous pouvez avoir des bibliothèques partagées plus petites et plus rapides en
n'exportant que JNI_OnLoad
. L'avantage de laisser l'environnement d'exécution découvrir
est qu'il nécessite un peu moins de code à écrire.
Pour utiliser RegisterNatives
:
- Fournissez une fonction
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - Dans votre
JNI_OnLoad
, enregistrez toutes vos méthodes natives à l'aide deRegisterNatives
. - Utilisez un script de version (recommandé) ou utilisez
-fvisibility=hidden
afin que seul votreJNI_OnLoad
est exportée depuis votre bibliothèque. Cela permet de générer un code plus rapide et moins volumineux, et d'éviter des collisions avec d'autres bibliothèques chargées dans votre application (mais cela crée des traces de pile moins utiles) ; si votre application plante en code natif).
L'initialiseur statique doit se présenter comme suit :
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
La fonction JNI_OnLoad
devrait se présenter comme suit :
écrite en C++:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. jclass c = env->FindClass("com/example/app/package/MyClass"); if (c == nullptr) return JNI_ERR; // Register your class' native methods. static const JNINativeMethod methods[] = { {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)}, {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)}, }; int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod)); if (rc != JNI_OK) return rc; return JNI_VERSION_1_6; }
Utiliser plutôt la "découverte" des méthodes natives, vous devez les nommer de façon spécifique (voir Spécification JNI pour plus de détails). Cela signifie que si une signature de méthode est incorrecte, vous ne le saurez que la première fois que la méthode sera réellement appelée.
Tous les appels FindClass
effectués à partir de JNI_OnLoad
résoudront les classes dans le
du chargeur de classe utilisé pour charger la bibliothèque partagée. Lorsqu'il est appelé à partir d'autres contextes, FindClass
utilise le chargeur de classe associé à la méthode en haut de la pile Java. S'il n'y en a pas (parce que l'appel provient d'un thread natif qui vient d'être associé), il utilise le chargeur de classe "system". Le chargeur de classe système ne connaît pas les classes de votre application. Vous ne pourrez donc pas rechercher vos propres classes avec FindClass
dans ce contexte. JNI_OnLoad
est donc un endroit pratique pour rechercher et mettre en cache des classes : une fois que vous disposez d'une référence globale jclass
valide, vous pouvez l'utiliser à partir de n'importe quel thread associé.
Appels natifs plus rapides avec @FastNative
et @CriticalNative
Les méthodes natives peuvent être annotées avec @FastNative
ou @CriticalNative
(mais pas les deux) pour accélérer les transitions entre le code géré et le code natif. Toutefois, ces annotations s'accompagnent de certains changements de comportement qui doivent être soigneusement examinés avant d'être utilisés. Nous mentionnons brièvement ces modifications ci-dessous, mais veuillez consulter la documentation pour en savoir plus.
L'annotation @CriticalNative
ne peut être appliquée qu'aux méthodes natives qui ne
utiliser des objets gérés (dans les paramètres ou les valeurs de retour, ou en tant que this
implicite) ;
modifie l'ABI de transition JNI. L'implémentation native doit exclure les paramètres JNIEnv
et jclass
de sa signature de fonction.
Lors de l'exécution d'une méthode @FastNative
ou @CriticalNative
, la récupération
la collecte de données ne peut pas suspendre le fil de discussion pour des tâches essentielles et peut être bloquée. Ne les utilisez pas.
annotations pour les méthodes de longue durée, y compris les méthodes généralement rapides, mais généralement illimitées.
En particulier, le code ne doit pas effectuer d'opérations d'E/S importantes ni acquérir des verrous natifs qui
peut être conservée
pendant longtemps.
Ces annotations ont été implémentées pour être utilisées par le système
Android 8
et a été testée par CTS
dans Android 14. Ces optimisations sont susceptibles de fonctionner également sur les appareils Android 8 à 13 (bien que
sans les garanties CTS fortes), mais la recherche dynamique de méthodes natives n'est prise en charge que
Android 12 et versions ultérieures, l'enregistrement explicite avec JNI RegisterNatives
est strictement obligatoire
pour Android 8 à 11. Ces annotations sont ignorées sur Android 7, la non-concordance de l'ABI
pour @CriticalNative
entraînerait un marshaling d'arguments incorrect et entraînerait probablement des plantages.
Pour les méthodes critiques en termes de performances qui ont besoin de ces annotations, il est fortement recommandé d'enregistrer explicitement la ou les méthodes avec JNI RegisterNatives
au lieu de s'appuyer sur la "découverte" des méthodes natives basée sur le nom. Pour optimiser les performances de démarrage de l'appli, il est recommandé
pour inclure les appelants des méthodes @FastNative
ou @CriticalNative
dans
profil de référence. Depuis Android 12, un appel à une méthode native @CriticalNative
à partir d'une méthode gérée compilée est presque aussi économique qu'un appel non intégré en C/C++ tant que tous les arguments tiennent dans les registres (par exemple, jusqu'à huit arguments entiers et huit arguments à virgule flottante sur arm64).
Parfois, il peut être préférable de diviser une méthode native en deux, une méthode très rapide et un autre qui gère les cas lents. Exemple :
Kotlin
fun writeInt(nativeHandle: Long, value: Int) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value) } } @CriticalNative external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean external fun nativeWriteInt(nativeHandle: Long, value: Int)
Java
void writeInt(long nativeHandle, int value) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value); } } @CriticalNative static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value); static native void nativeWriteInt(long nativeHandle, int value);
Considérations relatives au 64 bits
Pour prendre en charge les architectures qui utilisent des pointeurs 64 bits, utilisez un champ long
plutôt qu'un champ
int
lors du stockage d'un pointeur vers une structure native dans un champ Java.
Fonctionnalités non compatibles/Rétrocompatibilité
Toutes les fonctionnalités JNI 1.6 sont acceptées, à l'exception de la suivante :
DefineClass
n'est pas implémenté. Android n'utilise pas Des bytecodes Java ou fichiers de classe, ce qui permet de transmettre des données de classe binaire ne fonctionne pas.
Pour assurer la rétrocompatibilité avec les anciennes versions d'Android, vous devrez peut-être gardez à l'esprit les points suivants:
- Recherche dynamique de fonctions natives
Jusqu'à Android 2.0 (Eclair), le signe "$" le caractère n'était pas correct converti en "_00024" lors de la recherche de noms de méthodes. Opération en cours autour de cela nécessite d'utiliser un enregistrement explicite ou de déplacer des méthodes natives à partir des classes internes.
- Dissocier des threads
Avant Android 2.0 (Eclair), il n'était pas possible d'utiliser un
pthread_key_create
. destructor pour éviter que le "thread doit être dissocié avant sortie" vérifier. (L'environnement d'exécution utilise également une fonction de destructeur de clé pthread. Il s'agirait donc d'une course pour voir laquelle est appelée en premier.) - Références mondiales faibles
Jusqu'à Android 2.2 (Froyo), les références mondiales faibles n'étaient pas implémentées. Les anciennes versions refusent énergiquement les tentatives d'utilisation. Vous pouvez utiliser les constantes de version de la plate-forme Android pour tester la compatibilité.
Avant Android 4.0 (Ice Cream Sandwich), les références mondiales faibles ne pouvaient être transmis à
NewLocalRef
,NewGlobalRef
etDeleteWeakGlobalRef
Notez que cette spécification encourage vivement afin que les programmeurs créent des références réelles aux éléments généraux faibles avant d'effectuer quoi que ce soit avec eux, donc cela ne doit pas être du tout limitant.)À partir d'Android 4.0 (Ice Cream Sandwich), les références mondiales faibles peuvent comme toute autre référence JNI.
- Références locales
Jusqu'à Android 4.0 (Ice Cream Sandwich), les références locales étaient en fait des pointeurs directs. Ice Cream Sandwich a ajouté l'indirection nécessaires pour gérer de meilleurs récupérateurs de mémoire, mais cela implique des bugs JNI sont indétectables dans les anciennes versions. Pour en savoir plus, consultez la section Modifications des références locales JNI dans ICS.
Dans les versions d'Android antérieures à Android 8.0, le nombre de références locales est limité à une limite spécifique à la version. À partir d'Android 8.0, Android prend en charge un nombre illimité de références locales.
- Déterminer un type de référence avec
GetObjectRefType
Jusqu'à Android 4.0 (Ice Cream Sandwich), en raison de l'utilisation les pointeurs directs (voir ci-dessus), il était impossible d'implémenter
GetObjectRefType
correctement. Nous avons plutôt utilisé une méthode heuristique qui a passé en revue la table des variables globales faible, les arguments, table et la table des globales dans cet ordre. La première fois qu'il a détecté votre un pointeur direct, il signale que votre référence est du type en cours d'examen. Cela signifiait, par exemple, que si vous avez appeléGetObjectRefType
sur une classe jclass globale qui s'est produite identique à la classe jclass transmise en tant qu'argument implicite à votre classe native, vous obtenezJNILocalRefType
au lieu deJNIGlobalRefType
@FastNative
et@CriticalNative
Jusqu'à Android 7, ces annotations d'optimisation étaient ignorées. La non-correspondance de l'ABI pour
@CriticalNative
entraînerait un marshalling d'arguments incorrect et des plantages probables.la recherche dynamique de fonctions natives pour
@FastNative
et Les méthodes@CriticalNative
n'ont pas été implémentées dans Android 8 à 10 et contient des bugs connus dans Android 11. L'utilisation de ces optimisations sans un enregistrement explicite avec JNIRegisterNatives
est susceptible entraîner des plantages sur Android 8 à 11.FindClass
génèreClassNotFoundException
Pour assurer la rétrocompatibilité, Android génère
ClassNotFoundException
. au lieu deNoClassDefFoundError
lorsqu'une classe n'est pas trouvéeFindClass
Ce comportement est cohérent avec l'API de réflexion JavaClass.forName(name)
Questions fréquentes : Pourquoi UnsatisfiedLinkError
s'affiche-t-il ?
Lorsque vous travaillez sur du code natif, il n'est pas rare de voir un échec de ce type:
java.lang.UnsatisfiedLinkError: Library foo not found
Dans certains cas, cela signifie que la bibliothèque n'a pas été trouvée. Dans d'autres cas, la bibliothèque existe, mais elle n'a pas pu être ouverte par dlopen(3)
. Les détails de l'échec sont disponibles dans le message d'informations de l'exception.
Principales raisons pour lesquelles le message "Bibliothèque introuvable" peut s'afficher exceptions:
- La bibliothèque n'existe pas ou n'est pas accessible à l'application. Utilisez
adb shell ls -l <path>
pour vérifier sa présence et autorisations. - La bibliothèque n'a pas été créée avec le NDK. Cela peut entraîner dépendances à des fonctions ou des bibliothèques qui n'existent pas sur l'appareil.
Une autre classe d'échecs UnsatisfiedLinkError
ressemble à ceci:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
Dans logcat, vous verrez :
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
Cela signifie que l'environnement d'exécution a essayé de trouver une méthode correspondante, mais qu'il échec. Voici quelques raisons courantes:
- La bibliothèque ne se charge pas. Vérifiez la sortie logcat pour messages sur le chargement de la bibliothèque.
- La méthode est introuvable en raison d'une non-concordance du nom ou de la signature. Ce
est généralement causée par:
<ph type="x-smartling-placeholder">
- </ph>
- Pour la recherche de méthode paresseuse, ne pas déclarer de fonctions C++ avec
extern "C"
et une visibilité appropriée (JNIEXPORT
). Notez qu'avant Ice Cream Sandwich, la macro JNIEXPORT était incorrecte. Par conséquent, l'utilisation d'un nouveau GCC avec une anciennejni.h
ne fonctionnera pas. Vous pouvez utiliserarm-eabi-nm
pour afficher les symboles tels qu'ils apparaissent dans la bibliothèque. S'ils semblent tronqués (par exemple,_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
au lieu deJava_Foo_myfunc
) ou si le type de symbole est un "t" minuscule au lieu d'un "T" majuscule, vous devez ajuster la déclaration. - Pour l'enregistrement explicite, erreurs mineures lors de la saisie de la signature de la méthode. Assurez-vous que ce que vous transmettez à l'appel d'enregistrement correspond à la signature du fichier journal.
Rappelez-vous que le "B" est
byte
et "Z" estboolean
. Les composants de nom de classe dans les signatures commencent par "L", se terminent par ";", utiliser "/" pour séparer les noms des packages/classes et utiliser "$" pour séparer noms de classe internes (par exemple,Ljava/util/Map$Entry;
).
- Pour la recherche de méthode paresseuse, ne pas déclarer de fonctions C++ avec
Il peut être utile d'utiliser javah
pour générer automatiquement des en-têtes JNI
éviter certains problèmes.
Questions fréquentes : Pourquoi FindClass
n'a-t-il pas trouvé mon cours ?
(La plupart de ces conseils s'appliquent également
aux échecs de recherche de méthodes
avec GetMethodID
ou GetStaticMethodID
, ou des champs
avec GetFieldID
ou GetStaticFieldID
.)
Assurez-vous que la chaîne du nom de classe est au bon format. Les noms de classe JNI commencent par le nom du package et sont séparés par des barres obliques, comme java/lang/String
. Si vous recherchez une classe de tableau,
vous devez commencer par le nombre
approprié de crochets et
doit également encapsuler la classe avec "L" et ';', soit un tableau unidimensionnel de
String
serait [Ljava/lang/String;
.
Si vous recherchez une classe interne, utilisez "$" plutôt que ".". En général, l'utilisation de javap
sur le fichier .class est un bon moyen de connaître le nom interne de votre classe.
Si vous activez le rétrécissement du code, assurez-vous de configurer le code à conserver. Configurer des règles de conservation appropriées est importante, car le réducteur de code pourrait supprimer les classes, les méthodes, ou des champs qui ne sont utilisés qu'à partir de JNI.
Si le nom de la classe vous semble correct, il se peut que vous rencontriez un chargeur de classe.
problème. FindClass
souhaite démarrer la recherche de classe dans le chargeur de classe associé à votre code. Il examine la pile d'appel,
qui doit ressembler à ceci:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
La méthode la plus élevée est Foo.myfunc
. FindClass
recherche l'objet ClassLoader
associé à la classe Foo
et l'utilise.
Cela fonctionne généralement. Vous pouvez avoir des
ennuis si vous
créez vous-même un fil de discussion (en appelant pthread_create
, par exemple).
puis en l'attachant avec AttachCurrentThread
). Vous êtes maintenant
ne sont associés à aucun bloc de pile.
Si vous appelez FindClass
à partir de ce fil de discussion, la méthode
JavaVM démarrera dans le système au lieu de celui associé
avec votre application. Par conséquent, les tentatives de recherche de classes spécifiques à l'application échoueront.
Il existe plusieurs façons de contourner ce problème :
- Effectuez vos recherches
FindClass
une fois, dansJNI_OnLoad
et mettre en cache les références de classe pour plus tard utiliser. Tous les appelsFindClass
effectués lors de l'exécutionJNI_OnLoad
utilisera le chargeur de classe associé à la fonction qui a appeléSystem.loadLibrary
(il s'agit une règle spéciale fournie pour faciliter l'initialisation de la bibliothèque). Si le code de votre application charge la bibliothèque,FindClass
utilisera le chargeur de classe approprié. - Transmettez une instance de la classe aux fonctions qui doivent
en déclarant votre méthode native pour qu'elle accepte un argument Class et
puis transmettre
Foo.class
. - Mettre en cache une référence à l'objet
ClassLoader
quelque part et émettez directement des appelsloadClass
. Cela demande un certain effort.
Question fréquente: Comment partager des données brutes avec du code natif ?
Il se peut que vous ayez besoin d'accéder à un grand de données brutes provenant du code géré et du code natif. Il peut s'agir, par exemple, de la manipulation de bitmaps ou d'échantillons audio. Il existe deux approches de base.
Vous pouvez stocker les données dans un byte[]
. Cela permet un accès très rapide à partir du code géré. En revanche, du côté natif,
sans garantie de pouvoir accéder
aux données sans avoir à les copier. Dans certaines implémentations, GetByteArrayElements
et GetPrimitiveArrayCritical
renvoient des pointeurs réels vers les données brutes dans le tas de mémoire géré, mais dans d'autres, il alloue un tampon sur le tas de mémoire natif et copie les données.
L'alternative consiste à stocker les données dans un tampon d'octets direct. Ces
peuvent être créés avec java.nio.ByteBuffer.allocateDirect
; ou
la fonction JNI NewDirectByteBuffer
. Contrairement au
tampons d'octets, l'espace de stockage n'est pas alloué sur le tas géré et peut
toujours accessibles directement à partir du code natif (obtenir l'adresse
avec GetDirectBufferAddress
). En fonction de la
direction du
l'accès au tampon d'octets est implémenté, ce qui permet d'accéder aux données du code géré
peut être très lent.
Le choix de l'une ou l'autre dépend de deux facteurs :
- La plupart des accès aux données se feront-ils à partir de code écrit en Java ? ou en C/C++ ?
- Si les données sont finalement transmises à une API système, quel
est-ce qu'elle doit être incluse ? (Par exemple, si les données sont finalement transmises à un
qui prend un octet[], effectuant le traitement de façon directe
ByteBuffer
n'est peut-être pas judicieux.)
En l'absence de vainqueur évident, utilisez un tampon d'octets direct. leur soutien ; est intégré directement à JNI et les performances devraient s'améliorer dans les prochaines versions.