Conseils sur JNI

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 ou JNI_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 de RegisterNatives.
  • Compilez avec -fvisibility=hidden pour 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 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 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 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 obtenez JNILocalRefType au lieu de JNIGlobalRefType

  • @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 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)

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 ancien jni.h ne fonctionnera pas. Vous pouvez utiliser arm-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 de Java_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" 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.

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, 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. 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:

  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 à 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.