Conseils sur JNI

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 ou JNI_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 de RegisterNatives.
  • Utilisez un script de version (recommandé) ou utilisez -fvisibility=hidden afin que seul votre JNI_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 et DeleteWeakGlobalRef 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 obtenez JNILocalRefType au lieu de JNIGlobalRefType

  • @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 JNI RegisterNatives est susceptible entraîner des plantages sur Android 8 à 11.

  • FindClass génère ClassNotFoundException

    Pour assurer la rétrocompatibilité, Android génère ClassNotFoundException. au lieu de NoClassDefFoundError lorsqu'une classe n'est pas trouvée FindClass Ce comportement est cohérent avec l'API de réflexion Java Class.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 ancienne jni.h ne fonctionnera pas. Vous pouvez utiliser arm-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 de Java_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" est boolean. 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;).

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, dans JNI_OnLoad et mettre en cache les références de classe pour plus tard utiliser. Tous les appels FindClass effectués lors de l'exécution JNI_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 appels loadClass. 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 :

  1. La plupart des accès aux données se feront-ils à partir de code écrit en Java ? ou en C/C++ ?
  2. 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.