JNI est l'interface native de Java. Il définit un moyen pour le bytecode à partir duquel Android compile du code géré (écrit dans les langages de programmation Java ou Kotlin) pour interagir avec le code natif. (écrite en C/C++). JNI est neutre du fournisseur et permet de charger du code à partir de données partagées les bibliothèques, et même si elle est parfois encombrante 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 le Java Native Interface Specification pour avoir une idée du fonctionnement de JNI et des 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ù ces références sont créées et supprimées, utilisez la vue Tas de mémoire JNI dans le Profileur de mémoire dans Android Studio 3.2 et versions ultérieures.
Conseils généraux
Essayez de minimiser l'empreinte de votre couche JNI. Plusieurs dimensions sont à prendre en compte. Votre solution JNI doit essayer de suivre ces directives (répertoriées ci-dessous par ordre d'importance, en commençant par le plus important):
- Minimisez le marshaling des ressources sur la couche JNI. Marshalling sur la couche JNI a des coûts importants. Essayez de concevoir une interface qui réduit au maximum les données que vous devez marshaler et la fréquence à laquelle vous devez les rassembler.
- Éviter les communications asynchrones entre le code écrit dans un environnement de programmation géré et du code écrit en C++ si possible. Cela facilitera la gestion de votre interface JNI. Vous pouvez généralement simplifier les tâches L'interface utilisateur est mise à jour en conservant la mise à jour asynchrone dans la même langue que celle de l'interface utilisateur. 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 ê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.) JavaVM fournit l'"interface d'appel" fonctions, qui vous permettent de créer et de détruire une VM Java. 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 en tant que
le premier argument, sauf pour les méthodes @CriticalNative
,
permettent d'obtenir des appels natifs plus rapides.
JNIEnv est utilisé pour le stockage thread local. Pour cette raison, 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 y en a une ; voir AttachCurrentThread
ci-dessous).
Les déclarations C de JNIEnv et JavaVM sont différentes de celles de 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 commençant 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 disposerez ainsi d'un espace de pile suffisant,
dans le bon ThreadGroup
et que vous utilisez 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 dans
du code natif (consultez pthread_setname_np()
si vous avez un pthread_t
ou
thread_t
et std::thread::native_handle()
si vous avez un
std::thread
et souhaitez un pthread_t
).
L'association d'un thread créé en mode natif entraîne une erreur java.lang.Thread
à construire et à ajouter à la partie "main" ThreadGroup
,
pour le rendre visible par 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 connectés via JNI doivent appeler
DetachCurrentThread()
avant de quitter.
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
Si vous souhaitez accéder au champ d'un objet à partir du 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. Leur recherche peut nécessiter plusieurs chaînes une fois que vous les avez récupérées, 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.
La validité des références de classe, des ID de champ et des ID de méthode est garantie jusqu'à ce que la classe soit déchargé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
l'/le/la jclass
est une référence de classe qui doit être protégée par un appel.
à NewGlobalRef
(voir la section suivante).
Si vous souhaitez mettre en cache les ID lors du chargement d'une classe et les remettre automatiquement en cache si la classe est déchargée et rechargée, la bonne façon d'initialiser l'ID consiste à ajouter à la classe appropriée un extrait de code semblable à celui-ci:
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 et
puis rechargée, elle sera exécutée à nouveau.
Références locales et internationales
Tous les arguments 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 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 avertit de la plupart des utilisations incorrectes de références lors d'une extension JNI étendue
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 requête "globale" référence. 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 les 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 voir si deux références font référence au même objet,
vous devez utiliser la fonction IsSameObject
. Ne jamais comparer
Références contenant ==
en code natif.
L'une des conséquences
de cela est que vous
ne doivent pas supposer que les références d'objets sont constantes ou uniques
en code natif. La valeur représentant un objet peut être différente
d'un appel de méthode à l'autre, et il est possible que deux
différents objets peuvent avoir 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" les 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 général, toute annonce native
qui crée des références locales dans une boucle doit probablement exécuter
de suppression.
Soyez prudent lorsque vous utilisez 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 plus de commodité, JNI fournit des méthodes qui fonctionnent avec Code UTF-8 modifié également. La L'encodage modifié est utile pour le code C, car il encode \u0000 en tant que 0xc0 0x80 au lieu de 0x00. L'avantage, c'est que vous pouvez compter sur des chaînes se terminant par zéro de style C, convient à une 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 vers JNI et s’attendent à ce qu’il fonctionne correctement.
Pour obtenir la représentation UTF-16 d'un élément 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
à l'aide d'un tampon alloué à la pile et de 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 ou non 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 de tableau n'est plus épinglé.
- Copier: les données sont recopiées. Le tampon contenant la copie est libéré.
JNI_COMMIT
- Réelle: n'a aucun effet.
- 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
des modifications et exécuter du code qui utilise le contenu du tableau, vous pouvez
capable de
ignore le commit no-op. Une autre raison possible pour
vérifier l'indicateur est
gestion efficace de JNI_ABORT
. Par exemple, vous
pouvez demander
obtenir un tableau, le modifier à sa place, transmettre des éléments à d'autres fonctions, et
puis supprimez les modifications. Si vous savez que JNI crée une nouvelle copie pour
vous, il n'est pas nécessaire de créer une autre propriété "modifiable" copier. 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'était
est alloué, la mémoire d'origine doit être épinglée et ne peut pas être déplacée
le récupérateur de mémoire.
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 et copie le premier octet (len
)
puis libère le tableau. En fonction du
l'implémentation, l'appel Get
permet d'épingler ou de copier le tableau
contenus.
Le code copie les données (pour une deuxième fois, par exemple), puis appelle Release
. Ici,
JNI_ABORT
garantit qu'il n'y a pas de 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 lorsqu'une exception est en attente.
Votre code doit remarquer l'exception (via la valeur renvoyée par 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 rechercher une exception, car la valeur renvoyée n'est pas
sera valide si une exception a été levé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
, puis supprimez-le avec
ExceptionClear
Comme d'habitude,
le rejet des exceptions sans les traiter
peut entraîner des problèmes.
Il n'existe pas de fonction intégrée 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
n'est pas nulle. Utilisez GetStringUTFChars
pour obtenir une valeur
à printf(3)
ou équivalent.
Contrôle étendu
JNI effectue très peu de vérification des erreurs. 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: effectuer un appel JNI entre un get "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ûreté du typage: renvoi d'un type incompatible à partir de votre méthode native (renvoi d'un StringBuilder à partir d'une méthode déclarée pour renvoyer une chaîne, par exemple).
- UTF-8: transmission d'une séquence d'octets Modified UTF-8 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 votre appareil est 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 l'un ou l'autre de ces 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 sur
activer CheckJNI uniquement
pour votre application. Notez que les outils de compilation Android le font automatiquement pour
certains types de compilation.
Bibliothèques natives
Vous pouvez charger du code natif à partir de bibliothèques partagées avec la
System.loadLibrary
En pratique, les anciennes versions d'Android comportaient des bogues dans PackageManager qui provoquaient l'installation et des bibliothèques natives n'est pas fiable. 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 la mention "non décorée" le nom de la bibliothèque,
Pour charger libfubar.so
, vous devez donc transmettre "fubar"
.
Si vous n'avez qu'une seule classe avec des méthodes natives, il est judicieux que l'appel de
System.loadLibrary
dans un initialiseur statique pour cette classe. Sinon, vous risquez
vous souhaitez effectuer l'appel depuis Application
pour vous assurer que la bibliothèque est toujours chargée.
et toujours chargé tôt.
L'environnement d'exécution peut trouver vos méthodes natives de deux façons. 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 créer des bibliothèques partagées plus petites et plus rapides
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
. - Compilez avec
-fvisibility=hidden
pour 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 fausse, vous ne le saurez pas tant que le la première fois que la méthode est 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. En cas d'appel depuis d'autres
, FindClass
utilise le chargeur de classe associé à la méthode en haut de l'interface
ou s'il n'en existe pas, car l'appel provient d'un thread natif qui vient d'être associé.
il utilise le
« système » de la classe Loader. Le chargeur de classe système n'a pas connaissance de l'état
classes. Vous ne pourrez donc pas rechercher vos propres classes avec FindClass
dans ce
le contexte. Cela fait de JNI_OnLoad
un emplacement pratique pour rechercher et mettre en cache des classes: une fois
vous disposez d'une référence globale valide pour jclass
.
vous pouvez l'utiliser à partir
de n'importe quel fil de discussion 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. Cependant, ces annotations
s’accompagnent de certains changements de comportement qui doivent
être soigneusement réfléchis avant utilisation. Bien que nous
mentionner brièvement ces changements ci-dessous. Pour en savoir plus, reportez-vous à la documentation.
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
JNIEnv
et jclass
de la signature de sa fonction.
Lors de l'exécution d'une méthode @FastNative
ou @CriticalNative
, il est inutile
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
peut être conservée
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 qui nécessitent ces annotations, il est fortement recommandé de
enregistrez explicitement la ou les méthodes avec JNI RegisterNatives
au lieu d'utiliser le
« découverte » basée sur le nom de méthodes natives. 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
qu'un appel non intégré en C/C++, à condition que tous les arguments tiennent dans des registres (par exemple, jusqu'à
8 arguments entiers et jusqu'à 8 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 prises en charge, à l'exception 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 destruction de clé pthread, ce serait donc une course pour savoir lequel est appelé 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 devrait 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. Voir <ph type="x-smartling-placeholder"></ph> Pour en savoir plus, consultez les modifications apportées aux références locales JNI dans ICS.
Dans les versions d'Android antérieures à Android 8.0, le le nombre de références locales est limité à une version spécifique. À 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. L'ABI une incohérence pour
@CriticalNative
conduirait à un argument incorrect le marshaling et les 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)
Question fréquente: Pourquoi est-ce que je bénéficie de UnsatisfiedLinkError
?
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 est introuvable. Dans
autres cas, la bibliothèque existe, mais dlopen(3)
n'a pas pu l'ouvrir, et
Les détails de l'échec se trouvent
dans le message détaillé 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 n'est pas chargée. 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 différée, échec de déclaration des fonctions C++
avec
extern "C"
et les valeurs visibilité (JNIEXPORT
). Notez qu'avant l'utilisation de la glace, Sandwich, la macro JNIEXPORT était incorrecte, donc l'utilisation d'un nouveau GCC avec un ancienjni.h
ne fonctionnera pas. Vous pouvez utiliserarm-eabi-nm
pour voir les symboles tels qu'ils apparaissent dans la bibliothèque ; s'ils ont l'air (_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
, par exemple) au lieu deJava_Foo_myfunc
), ou si le type de symbole est un "t" minuscule plutôt qu'un "T" majuscule, vous devez ajuster la déclaration. - Dans le cas d'un enregistrement explicite, des erreurs mineures lors de la saisie du
signature de la méthode. Assurez-vous que les données que vous transmettez
l'appel d'enregistrement correspond à
la signature dans le 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 différée, échec de déclaration des fonctions C++
avec
Il peut être utile d'utiliser javah
pour générer automatiquement des en-têtes JNI
éviter certains problèmes.
Question fréquente: 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. Classe JNI
les noms 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 découvrir
nom interne de votre cours.
Si vous activez la minification de code, assurez-vous 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 lancer la recherche de classe dans
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é au Foo
et l'utilise.
Cela fait généralement ce que vous voulez. 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 moyens 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
. Pour cela, demande beaucoup d'efforts.
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. Exemples courants incluent la manipulation de bitmaps ou d'échantillons sonores. Il y a deux approches de base.
Vous pouvez stocker les données dans un byte[]
. Cela permet d'accélérer le processus
à partir du code géré. Côté natif,
sans garantie de pouvoir accéder
aux données sans avoir à les copier. Dans
certaines implémentations, GetByteArrayElements
et
GetPrimitiveArrayCritical
renvoie les pointeurs réels vers
des données brutes dans le tas géré, mais dans d'autres, un tampon sera alloué
sur le tas de mémoire natif et y copier 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'option à utiliser 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 à terme transmises à une API système, sous quel format
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.