Partager via


Utilisation de JNI et de Xamarin.Android

Xamarin.Android permet d’écrire des applications Android avec C# au lieu de Java. Plusieurs assemblys sont fournis avec Xamarin.Android qui fournissent des liaisons pour les bibliothèques Java, notamment Mono.Android.dll et Mono.Android.GoogleMaps.dll. Toutefois, les liaisons ne sont pas fournies pour chaque bibliothèque Java possible, et les liaisons fournies peuvent ne pas lier chaque type et membre Java. Pour utiliser des types et des membres Java non liés, l’interface java native (JNI) peut être utilisée. Cet article explique comment utiliser JNI pour interagir avec des types et des membres Java à partir d’applications Xamarin.Android.

Vue d’ensemble

Il n’est pas toujours nécessaire ou possible de créer un wrapper MCW (Managed Callable Wrapper) pour appeler du code Java. Dans de nombreux cas, JNI « inline » est parfaitement acceptable et utile pour l’utilisation ponctuelle de membres Java non liés. Il est souvent plus simple d’utiliser JNI pour appeler une seule méthode sur une classe Java que de générer une liaison .jar entière.

Xamarin.Android fournit l’assembly Mono.Android.dll , qui fournit une liaison pour la bibliothèque d’Android android.jar . Les types et membres non présents dans Mono.Android.dll et les types non présents dans android.jar peuvent être utilisés en les liant manuellement. Pour lier des types et des membres Java, vous utilisez l’interface java native (JNI) pour rechercher des types, lire et écrire des champs et appeler des méthodes.

L’API JNI dans Xamarin.Android est conceptuellement très similaire à l’API System.Reflection dans .NET : elle vous permet de rechercher des types et des membres par nom, lire et écrire des valeurs de champ, appeler des méthodes, etc. Vous pouvez utiliser JNI et l’attribut Android.Runtime.RegisterAttribute personnalisé pour déclarer des méthodes virtuelles qui peuvent être liées à la prise en charge de la substitution. Vous pouvez lier des interfaces afin qu’elles puissent être implémentées en C#.

Ce document explique :

  • Comment JNI fait référence aux types.
  • Comment rechercher, lire et écrire des champs.
  • Comment rechercher et appeler des méthodes.
  • Comment exposer des méthodes virtuelles pour autoriser la substitution à partir de code managé.
  • Comment exposer des interfaces.

Spécifications

JNI, tel qu’exposé via l’espace de noms Android.Runtime.JNIEnv, est disponible dans chaque version de Xamarin.Android. Pour lier des types et interfaces Java, vous devez utiliser Xamarin.Android 4.0 ou version ultérieure.

Wrappers callables managés

Un wrapper MCW (Managed Callable Wrapper) est une liaison pour une classe ou une interface Java qui encapsule toutes les machines JNI afin que le code C# du client n’ait pas à se soucier de la complexité sous-jacente de JNI. La plupart des Mono.Android.dll éléments se composent de wrappers pouvant être callables managés.

Les wrappers pouvant être callables managés ont deux objectifs :

  1. Encapsulez l’utilisation de JNI afin que le code client n’ait pas besoin de connaître la complexité sous-jacente.
  2. Rendre possible la sous-classe des types Java et implémenter des interfaces Java.

Le premier objectif est purement pratique et l’encapsulation de la complexité afin que les consommateurs disposent d’un ensemble simple et managé de classes à utiliser. Cela nécessite l’utilisation des différents membres JNIEnv , comme décrit plus loin dans cet article. N’oubliez pas que les wrappers pouvant être appelé gérés ne sont pas strictement nécessaires : l’utilisation de JNI « inline » est parfaitement acceptable et est utile pour l’utilisation ponctuelle de membres Java non liés. L’implémentation de sous-classes et d’interface nécessite l’utilisation de wrappers pouvant être appelé managés.

Wrappers pouvant être appelés par Android

Les wrappers pouvant être appelés Android (ACW) sont requis chaque fois que le runtime Android (ART) doit appeler du code managé ; ces wrappers sont requis, car il n’existe aucun moyen d’inscrire des classes auprès de ART au moment de l’exécution. (Plus précisément, la fonction JNI DefineClass n’est pas prise en charge par le runtime Android. Les wrappers pouvant être appelé Android permettent ainsi de compenser l’absence de prise en charge de l’inscription de type runtime.)

Chaque fois que le code Android doit exécuter une méthode virtuelle ou d’interface qui est remplacée ou implémentée dans le code managé, Xamarin.Android doit fournir un proxy Java afin que cette méthode soit distribuée au type managé approprié. Ces types de proxy Java sont du code Java qui ont la « même » classe de base et la même liste d’interface Java que le type managé, implémentant les mêmes constructeurs et déclarant toutes les méthodes d’interface et de classe de base remplacées.

Les wrappers pouvant être appelé Android sont générés par le programme monodroid.exe pendant le processus de génération et sont générés pour tous les types qui héritent (directement ou indirectement) de Java.Lang.Object.

Implémentation des interfaces

Il arrive que vous deviez implémenter une interface Android (telle que Android.Content.IComponentCallbacks).

Toutes les classes et interfaces Android étendent l’interface Android.Runtime.IJavaObject ; par conséquent, tous les types Android doivent implémenter IJavaObject. Xamarin.Android tire parti de ce fait : il utilise IJavaObject pour fournir à Android un proxy Java (un wrapper pouvant être appelé Android) pour le type managé donné. Étant donné quemonodroid.exe recherche Java.Lang.Object uniquement les sous-classes (qui doivent implémenter IJavaObject), la sous-classe Java.Lang.Object nous fournit un moyen d’implémenter des interfaces dans du code managé. Par exemple :

class MyComponentCallbacks : Java.Lang.Object, Android.Content.IComponentCallbacks {
    public void OnConfigurationChanged (Android.Content.Res.Configuration newConfig) {
        // implementation goes here...
    }
    public void OnLowMemory () {
        // implementation goes here...
    }
}

Informations d’implémentation

Le reste de cet article fournit les détails de l’implémentation pouvant être modifiés sans préavis (et est présenté ici uniquement parce que les développeurs peuvent être curieux de savoir ce qui se passe sous le capot).

Par exemple, compte tenu de la source C# suivante :

using System;
using Android.App;
using Android.OS;

namespace Mono.Samples.HelloWorld
{
    public class HelloAndroid : Activity
    {
        protected override void OnCreate (Bundle savedInstanceState)
        {
            base.OnCreate (savedInstanceState);
            SetContentView (R.layout.main);
        }
    }
}

Le programme mandroid.exe génère le wrapper callable Android suivant :

package mono.samples.helloWorld;

public class HelloAndroid extends android.app.Activity {
    static final String __md_methods;
    static {
        __md_methods =
            "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" +
            "";
        mono.android.Runtime.register (
                "Mono.Samples.HelloWorld.HelloAndroid, HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                HelloAndroid.class,
                __md_methods);
    }

    public HelloAndroid ()
    {
        super ();
        if (getClass () == HelloAndroid.class)
            mono.android.TypeManager.Activate (
                "Mono.Samples.HelloWorld.HelloAndroid, HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                "", this, new java.lang.Object[] { });
    }

    @Override
    public void onCreate (android.os.Bundle p0)
    {
        n_onCreate (p0);
    }

    private native void n_onCreate (android.os.Bundle p0);
}

Notez que la classe de base est conservée et que des déclarations de méthode natives sont fournies pour chaque méthode remplacée dans le code managé.

ExportAttribute et ExportFieldAttribute

En règle générale, Xamarin.Android génère automatiquement le code Java qui comprend l’ACW ; cette génération est basée sur les noms de classe et de méthode lorsqu’une classe dérive d’une classe Java et remplace les méthodes Java existantes. Toutefois, dans certains scénarios, la génération de code n’est pas adéquate, comme indiqué ci-dessous :

  • Android prend en charge les noms d’actions dans les attributs XML de disposition, par exemple l’attribut XML android:onClick . Lorsqu’elle est spécifiée, la vue gonflée instance tente de rechercher la méthode Java.

  • L’interface java.io.Serializable requiert readObject les méthodes etwriteObject. Étant donné qu’elles ne sont pas membres de cette interface, notre implémentation managée correspondante n’expose pas ces méthodes au code Java.

  • L’interface android.os.Parcelable s’attend à ce qu’une classe d’implémentation ait un champ CREATOR statique de type Parcelable.Creator. Le code Java généré nécessite un champ explicite. Avec notre scénario standard, il n’existe aucun moyen de générer un champ dans du code Java à partir de code managé.

Étant donné que la génération de code ne fournit pas de solution pour générer des méthodes Java arbitraires avec des noms arbitraires, à compter de Xamarin.Android 4.2, exportAttribute et ExportFieldAttribute ont été introduits pour offrir une solution aux scénarios ci-dessus. Les deux attributs résident dans l’espace de Java.Interop noms :

  • ExportAttribute : spécifie un nom de méthode et ses types d’exception attendus (pour donner des « levées » explicites dans Java). Lorsqu’elle est utilisée sur une méthode, la méthode « exporte » une méthode Java qui génère un code de répartition vers l’appel JNI correspondant à la méthode managée. Il peut être utilisé avec android:onClick et java.io.Serializable.

  • ExportFieldAttribute : spécifie un nom de champ. Il réside sur une méthode qui fonctionne comme initialiseur de champ. Vous pouvez l’utiliser avec android.os.Parcelable.

Résolution des problèmes d’ExportAttribute et d’ExportFieldAttribute

  • L’empaquetage échoue en raison d’unMono.Android.Export.dll manquant : si vous avez utilisé ExportAttribute ou ExportFieldAttribute sur certaines méthodes dans votre code ou des bibliothèques dépendantes, vous devez ajouter Mono.Android.Export.dll. Cet assembly est isolé pour prendre en charge le code de rappel à partir de Java. Elle est distincte de Mono.Android.dll car elle ajoute une taille supplémentaire à l’application.

  • Dans Build Release, MissingMethodException se produit pour les méthodes d’exportation : dans la build Release, MissingMethodException se produit pour les méthodes d’exportation. (Ce problème est résolu dans la dernière version de Xamarin.Android.)

ExportParameterAttribute

ExportAttribute et ExportFieldAttribute fournissent des fonctionnalités que le code d’exécution Java peut utiliser. Ce code d’exécution accède au code managé via les méthodes JNI générées pilotées par ces attributs. Par conséquent, il n’existe aucune méthode Java que la méthode managée lie ; par conséquent, la méthode Java est générée à partir d’une signature de méthode managée.

Toutefois, cette affaire n’est pas entièrement déterminante. Plus particulièrement, cela est vrai dans certains mappages avancés entre les types managés et les types Java, tels que :

  • InputStream
  • OutputStream
  • XmlPullParser
  • XmlResourceParser

Lorsque des types tels que ceux-ci sont nécessaires pour les méthodes exportées, le ExportParameterAttribute doit être utilisé pour donner explicitement au paramètre ou à la valeur de retour correspondant un type.

Attribut d’annotation

Dans Xamarin.Android 4.2, nous avons converti IAnnotation des types d’implémentation en attributs (System.Attribute) et ajouté la prise en charge de la génération d’annotations dans les wrappers Java.

Cela signifie les modifications directionnelles suivantes :

  • Le générateur de liaison génère Java.Lang.DeprecatedAttribute à partir de java.Lang.Deprecated (alors qu’il doit être [Obsolete] dans du code managé).

  • Cela ne signifie pas que la classe existante Java.Lang.Deprecated disparaîtra. Ces objets basés sur Java peuvent toujours être utilisés comme des objets Java habituels (si une telle utilisation existe). Il y aura Deprecated des classes et DeprecatedAttribute .

  • La Java.Lang.DeprecatedAttribute classe est marquée comme [Annotation] . Lorsqu’un attribut personnalisé est hérité de cet [Annotation] attribut, la tâche msbuild génère une annotation Java pour cet attribut personnalisé (@Deprecated) dans le wrapper callable Android (ACW).

  • Les annotations peuvent être générées sur des classes, des méthodes et des champs exportés (qui est une méthode dans le code managé).

Si la classe contenante (la classe annotée elle-même ou la classe qui contient les membres annotés) n’est pas inscrite, la source de classe Java entière n’est pas générée du tout, y compris les annotations. Pour les méthodes, vous pouvez spécifier pour ExportAttribute obtenir la méthode explicitement générée et annotée. En outre, il ne s’agit pas d’une fonctionnalité permettant de « générer » une définition de classe d’annotation Java. En d’autres termes, si vous définissez un attribut managé personnalisé pour une annotation donnée, vous devez ajouter une autre bibliothèque .jar qui contient la classe d’annotation Java correspondante. L’ajout d’un fichier source Java qui définit le type d’annotation n’est pas suffisant. Le compilateur Java ne fonctionne pas de la même manière qu’apt.

En outre, les limitations suivantes s’appliquent :

  • Ce processus de conversion ne prend pas en compte @Target l’annotation sur le type d’annotation jusqu’à présent.

  • Les attributs d’une propriété ne fonctionnent pas. Utilisez plutôt des attributs pour la propriété getter ou setter.

Liaison de classes

La liaison d’une classe signifie écrire un wrapper pouvant être appelé managé pour simplifier l’appel du type Java sous-jacent.

La liaison de méthodes virtuelles et abstraites pour autoriser la substitution à partir de C# nécessite Xamarin.Android 4.0. Toutefois, n’importe quelle version de Xamarin.Android peut lier des méthodes non virtuelles, des méthodes statiques ou des méthodes virtuelles sans prendre en charge les remplacements.

Une liaison contient généralement les éléments suivants :

Déclaration de type Handle

Les méthodes de recherche de champ et de méthode nécessitent une référence d’objet faisant référence à leur type de déclaration. Par convention, cela est conservé dans un class_ref champ :

static IntPtr class_ref = JNIEnv.FindClass(CLASS);

Pour plus d’informations sur le jeton, consultez la section Références de CLASStype JNI.

Champs de liaison

Les champs Java sont exposés en tant que propriétés C#, par exemple le champ Java java.lang.System.in est lié en tant que propriété C# Java.Lang.JavaSystem.In. En outre, comme JNI fait la distinction entre les champs statiques et les champs instance, différentes méthodes sont utilisées lors de l’implémentation des propriétés.

La liaison de champs implique trois ensembles de méthodes :

  1. Méthode d’id de champ get . La méthode get field id est chargée de renvoyer un handle de champ que les méthodes get field value et set field value utiliseront. Pour obtenir l’ID de champ, vous devez connaître le type de déclaration, le nom du champ et la signature de type JNI du champ.

  2. Méthodes de valeur de champ get . Ces méthodes nécessitent le handle de champ et sont responsables de la lecture de la valeur du champ à partir de Java. La méthode à utiliser dépend du type du champ.

  3. Méthodes de valeur de champ définie . Ces méthodes nécessitent le handle de champ et sont responsables de l’écriture de la valeur du champ dans Java. La méthode à utiliser dépend du type du champ.

Les champs statiques utilisent les méthodes JNIEnv.GetStaticFieldID, JNIEnv.GetStatic*Fieldet JNIEnv.SetStaticField .

Les champs d’instance utilisent les méthodes JNIEnv.GetFieldID, JNIEnv.Get*Fieldet JNIEnv.SetField .

Par exemple, la propriété JavaSystem.In statique peut être implémentée comme suit :

static IntPtr in_jfieldID;
public static System.IO.Stream In
{
    get {
        if (in_jfieldId == IntPtr.Zero)
            in_jfieldId = JNIEnv.GetStaticFieldID (class_ref, "in", "Ljava/io/InputStream;");
        IntPtr __ret = JNIEnv.GetStaticObjectField (class_ref, in_jfieldId);
        return InputStreamInvoker.FromJniHandle (__ret, JniHandleOwnership.TransferLocalRef);
    }
}

Remarque : Nous utilisons InputStreamInvoker.FromJniHandle pour convertir la référence JNI en instance System.IO.Stream , et nous utilisonsJniHandleOwnership.TransferLocalRef, car JNIEnv.GetStaticObjectField retourne une référence locale.

La plupart des types Android.Runtime ont des FromJniHandle méthodes qui convertissent une référence JNI en type souhaité.

Liaison de méthode

Les méthodes Java sont exposées en tant que méthodes C# et en tant que propriétés C#. Par exemple, la méthode Java java.lang.Runtime.runFinalizersOnExit est liée en tant que méthode Java.Lang.Runtime.RunFinalizersOnExit , et la méthode java.lang.Object.getClass est liée en tant que propriété Java.Lang.Object.Class .

L’appel de méthode est un processus en deux étapes :

  1. ID de méthode get pour la méthode à appeler. La méthode d’id de méthode get est chargée de renvoyer un handle de méthode que les méthodes d’appel de méthode utiliseront. Pour obtenir l’ID de méthode, vous devez connaître le type de déclaration, le nom de la méthode et la signature de type JNI de la méthode.

  2. Appelez la méthode.

Comme pour les champs, les méthodes à utiliser pour obtenir l’ID de méthode et appeler la méthode diffèrent entre les méthodes statiques et les méthodes instance.

Les méthodes statiques utilisent JNIEnv.GetStaticMethodID() pour rechercher l’ID de méthode et utiliser la JNIEnv.CallStatic*Method famille de méthodes pour l’appel.

Les méthodes d’instance utilisent JNIEnv.GetMethodID pour rechercher l’ID de méthode et utiliser les JNIEnv.Call*Method familles de méthodes et JNIEnv.CallNonvirtual*Method pour l’appel.

La liaison de méthode est potentiellement plus qu’un simple appel de méthode. La liaison de méthode inclut également l’autorisation de remplacer une méthode (pour les méthodes abstraites et non finales) ou d’être implémentée (pour les méthodes d’interface). La section Prise en charge de l’héritage, interfaces couvre les complexités de la prise en charge des méthodes d’interface et des méthodes d’interface virtuelles.

Méthodes statiques

La liaison d’une méthode statique implique d’utiliser JNIEnv.GetStaticMethodID pour obtenir un handle de méthode, puis d’utiliser la méthode appropriée JNIEnv.CallStatic*Method , en fonction du type de retour de la méthode. Voici un exemple de liaison pour la méthode Runtime.getRuntime :

static IntPtr id_getRuntime;

[Register ("getRuntime", "()Ljava/lang/Runtime;", "")]
public static Java.Lang.Runtime GetRuntime ()
{
    if (id_getRuntime == IntPtr.Zero)
        id_getRuntime = JNIEnv.GetStaticMethodID (class_ref,
                "getRuntime", "()Ljava/lang/Runtime;");

    return Java.Lang.Object.GetObject<Java.Lang.Runtime> (
            JNIEnv.CallStaticObjectMethod  (class_ref, id_getRuntime),
            JniHandleOwnership.TransferLocalRef);
}

Notez que nous stockons le handle de méthode dans un champ statique, id_getRuntime. Il s’agit d’une optimisation des performances, de sorte que le handle de méthode n’a pas besoin d’être recherché à chaque appel. Il n’est pas nécessaire de mettre en cache le handle de méthode de cette façon. Une fois le handle de méthode obtenu, JNIEnv.CallStaticObjectMethod est utilisé pour appeler la méthode. JNIEnv.CallStaticObjectMethodretourne un IntPtr qui contient le handle du instance Java retourné. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) est utilisé pour convertir le handle Java en objet fortement typé instance.

Liaison de méthode d’instance non virtuelle

La liaison d’une final méthode instance ou d’une méthode instance qui ne nécessite pas de substitution implique d’utiliser JNIEnv.GetMethodID pour obtenir un handle de méthode, puis d’utiliser la méthode appropriéeJNIEnv.Call*Method, en fonction du type de retour de la méthode. Voici un exemple de liaison pour la Object.Class propriété :

static IntPtr id_getClass;
public Java.Lang.Class Class {
    get {
        if (id_getClass == IntPtr.Zero)
            id_getClass = JNIEnv.GetMethodID (class_ref, "getClass", "()Ljava/lang/Class;");
        return Java.Lang.Object.GetObject<Java.Lang.Class> (
                JNIEnv.CallObjectMethod (Handle, id_getClass),
                JniHandleOwnership.TransferLocalRef);
    }
}

Notez que nous stockons le handle de méthode dans un champ statique, id_getClass. Il s’agit d’une optimisation des performances, de sorte que le handle de méthode n’a pas besoin d’être recherché à chaque appel. Il n’est pas nécessaire de mettre en cache le handle de méthode de cette façon. Une fois le handle de méthode obtenu, JNIEnv.CallStaticObjectMethod est utilisé pour appeler la méthode. JNIEnv.CallStaticObjectMethodretourne un IntPtr qui contient le handle du instance Java retourné. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) est utilisé pour convertir le handle Java en objet fortement typé instance.

Constructeurs de liaison

Les constructeurs sont des méthodes Java portant le nom "<init>". Comme avec les méthodes java instance, JNIEnv.GetMethodID est utilisé pour rechercher le handle du constructeur. Contrairement aux méthodes Java, les méthodes JNIEnv.NewObject sont utilisées pour appeler le handle de méthode du constructeur. La valeur de retour de JNIEnv.NewObject est une référence locale JNI :

int value = 42;
IntPtr class_ref    = JNIEnv.FindClass ("java/lang/Integer");
IntPtr id_ctor_I    = JNIEnv.GetMethodID (class_ref, "<init>", "(I)V");
IntPtr lrefInstance = JNIEnv.NewObject (class_ref, id_ctor_I, new JValue (value));
// Dispose of lrefInstance, class_ref…

Normalement, une liaison de classe sous-classe Java.Lang.Object. Lors de Java.Lang.Objectla sous-classe , une sémantique supplémentaire entre en jeu : une Java.Lang.Object instance conserve une référence globale à un instance Java via la Java.Lang.Object.Handle propriété .

  1. Le Java.Lang.Object constructeur par défaut alloue un instance Java.

  2. Si le type a un RegisterAttribute et RegisterAttribute.DoNotGenerateAcw est true , une instance du RegisterAttribute.Name type est créée via son constructeur par défaut.

  3. Sinon, le wrapper callable Android (ACW) correspondant à this.GetType est instancié via son constructeur par défaut. Les wrappers Pouvant appeler Android sont générés lors de la création du package pour chaque Java.Lang.Object sous-classe pour laquelle n’est RegisterAttribute.DoNotGenerateAcw pas défini sur true.

Pour les types qui ne sont pas des liaisons de classe, il s’agit de la sémantique attendue : l’instanciation d’un Mono.Samples.HelloWorld.HelloAndroid instance C# doit construire un instance Java mono.samples.helloworld.HelloAndroid qui est un wrapper callable Android généré.

Pour les liaisons de classes, il peut s’agir du comportement correct si le type Java contient un constructeur par défaut et/ou qu’aucun autre constructeur ne doit être appelé. Sinon, un constructeur doit être fourni qui effectue les actions suivantes :

  1. Appel de Java.Lang.Object(IntPtr, JniHandleOwnership) au lieu du constructeur par défaut Java.Lang.Object . Cela est nécessaire pour éviter de créer une nouvelle instance Java.

  2. Vérifiez la valeur de Java.Lang.Object.Handle avant de créer des instances Java. La Object.Handle propriété aura une valeur autre que IntPtr.Zero si un wrapper appelant Android a été construit dans du code Java et si la liaison de classe est en cours de construction pour contenir le wrapper Android Callable Wrapper créé instance. Par exemple, quand Android crée un mono.samples.helloworld.HelloAndroid instance, le wrapper Appelant Android est créé en premier et le constructeur Java HelloAndroid crée un instance du type correspondantMono.Samples.HelloWorld.HelloAndroid, la Object.Handle propriété étant définie sur le instance Java avant l’exécution du constructeur.

  3. Si le type d’exécution actuel n’est pas le même que le type de déclaration, un instance du wrapper Callable Android correspondant doit être créé et utiliser Object.SetHandle pour stocker le handle retourné par JNIEnv.CreateInstance.

  4. Si le type d’exécution actuel est le même que le type de déclaration, appelez le constructeur Java et utilisez Object.SetHandle pour stocker le handle retourné par JNIEnv.NewInstance .

Par exemple, considérez le constructeur java.lang.Integer(int). Ceci est lié comme suit :

// Cache the constructor's method handle for later use
static IntPtr id_ctor_I;

// Need [Register] for subclassing
// RegisterAttribute.Name is always ".ctor"
// RegisterAttribute.Signature is tye JNI type signature of constructor
// RegisterAttribute.Connector is ignored; use ""
[Register (".ctor", "(I)V", "")]
public Integer (int value)
    // 1. Prevent Object default constructor execution
    : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
{
    // 2. Don't allocate Java instance if already allocated
    if (Handle != IntPtr.Zero)
        return;

    // 3. Derived type? Create Android Callable Wrapper
    if (GetType () != typeof (Integer)) {
        SetHandle (
                Android.Runtime.JNIEnv.CreateInstance (GetType (), "(I)V", new JValue (value)),
                JniHandleOwnership.TransferLocalRef);
        return;
    }

    // 4. Declaring type: lookup &amp; cache method id...
    if (id_ctor_I == IntPtr.Zero)
        id_ctor_I = JNIEnv.GetMethodID (class_ref, "<init>", "(I)V");
    // ...then create the Java instance and store
    SetHandle (
            JNIEnv.NewObject (class_ref, id_ctor_I, new JValue (value)),
            JniHandleOwnership.TransferLocalRef);
}

Les méthodes JNIEnv.CreateInstance sont des aides pour effectuer un JNIEnv.FindClass, JNIEnv.GetMethodID, JNIEnv.NewObjectet JNIEnv.DeleteGlobalReference sur la valeur retournée à partir de JNIEnv.FindClass. Voir la section suivante pour plus de détails.

Prise en charge de l’héritage, interfaces

La sous-classification d’un type Java ou l’implémentation d’une interface Java nécessite la génération de wrappers Android Callable Wrappers (ACWs) qui sont générés pour chaque Java.Lang.Object sous-classe pendant le processus d’empaquetage. La génération ACW est contrôlée via l’attribut personnalisé Android.Runtime.RegisterAttribute .

Pour les types C#, le [Register] constructeur d’attribut personnalisé nécessite un argument : la référence de type simplifié JNI pour le type Java correspondant. Cela permet de fournir des noms différents entre Java et C#.

Avant Xamarin.Android 4.0, l’attribut [Register] personnalisé n’était pas disponible pour les types Java existants « alias ». En effet, le processus de génération ACW génère des ACL pour chaque Java.Lang.Object sous-classe rencontrée.

Xamarin.Android 4.0 a introduit la propriété RegisterAttribute.DoNotGenerateAcw . Cette propriété indique au processus de génération ACW d’ignorer le type annoté, ce qui autorise la déclaration de nouveaux wrappers pouvant être callables managés qui n’entraînent pas la génération de fichiers ACW au moment de la création du package. Cela permet de lier des types Java existants. Pour instance, considérez la classe Java simple suivante, Adder, qui contient une méthode, add, qui ajoute aux entiers et retourne le résultat :

package mono.android.test;
public class Adder {
    public int add (int a, int b) {
        return a + b;
    }
}

Le Adder type peut être lié comme suit :

[Register ("mono/android/test/Adder", DoNotGenerateAcw=true)]
public partial class Adder : Java.Lang.Object {
    static IntPtr class_ref = JNIEnv.FindClass ( "mono/android/test/Adder");

    public Adder ()
    {
    }

    public Adder (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
    }
}
partial class ManagedAdder : Adder {
}

Ici, le Adder type C# alias le Adder type Java. L’attribut [Register] est utilisé pour spécifier le nom JNI du mono.android.test.Adder type Java, et la propriété est utilisée pour empêcher la DoNotGenerateAcw génération d’ACW. Cela entraîne la génération d’un ACW pour le ManagedAdder type, qui sous-classe correctement le mono.android.test.Adder type. Si la RegisterAttribute.DoNotGenerateAcw propriété n’avait pas été utilisée, le processus de génération Xamarin.Android aurait généré un nouveau mono.android.test.Adder type Java. Cela entraînerait des erreurs de compilation, car le mono.android.test.Adder type serait présent deux fois, dans deux fichiers distincts.

Liaison de méthodes virtuelles

ManagedAdder sous-classes le type Java Adder , mais ce n’est pas particulièrement intéressant : le type C# Adder ne définit aucune méthode virtuelle, il ne peut donc ManagedAdder pas remplacer quoi que ce soit.

Les méthodes de liaison virtual pour autoriser le remplacement par des sous-classes nécessitent plusieurs opérations qui doivent être effectuées dans les deux catégories suivantes :

  1. Liaison de méthode

  2. Inscription de méthode

Liaison de méthode

Une liaison de méthode nécessite l’ajout de deux membres de support à la définition C Adder # : ThresholdTypeet ThresholdClass.

ThresholdType

La ThresholdType propriété retourne le type actuel de la liaison :

partial class Adder {
    protected override System.Type ThresholdType {
        get {
            return typeof (Adder);
        }
    }
}

ThresholdType est utilisé dans la liaison de méthode pour déterminer quand il doit effectuer une répartition de méthode virtuelle et non virtuelle. Elle doit toujours retourner une System.Type instance qui correspond au type C# déclarant.

ThresholdClass

La ThresholdClass propriété retourne la référence de classe JNI pour le type lié :

partial class Adder {
    protected override IntPtr ThresholdClass {
        get {
            return class_ref;
        }
    }
}

ThresholdClass est utilisé dans la liaison de méthode lors de l’appel de méthodes non virtuelles.

Implémentation de liaison

L’implémentation de la liaison de méthode est responsable de l’appel du runtime de la méthode Java. Elle contient également une déclaration d’attribut [Register] personnalisée qui fait partie de l’inscription de méthode et sera abordée dans la section Inscription de méthode :

[Register ("add", "(II)I", "GetAddHandler")]
    public virtual int Add (int a, int b)
    {
        if (id_add == IntPtr.Zero)
            id_add = JNIEnv.GetMethodID (class_ref, "add", "(II)I");
        if (GetType () == ThresholdType)
            return JNIEnv.CallIntMethod (Handle, id_add, new JValue (a), new JValue (b));
        return JNIEnv.CallNonvirtualIntMethod (Handle, ThresholdClass, id_add, new JValue (a), new JValue (b));
    }
}

Le id_add champ contient l’ID de méthode pour la méthode Java à appeler. La id_add valeur est obtenue à partir de JNIEnv.GetMethodID, ce qui nécessite la classe déclarante (class_ref), le nom de la méthode Java ("add") et la signature JNI de la méthode ("(II)I").

Une fois l’ID de méthode obtenu, GetType est comparé à ThresholdType pour déterminer si la répartition virtuelle ou non virtuelle est nécessaire. La répartition virtuelle est requise lorsque GetType correspond ThresholdTypeà , comme Handle peut faire référence à une sous-classe allouée par Java qui remplace la méthode .

Quand GetType ne correspond ThresholdTypepas à , Adder a été sous-classé (par exemple par ManagedAdder), et l’implémentation Adder.Add ne sera appelée que si la sous-classe a base.Addappelé . Il s’agit du cas de répartition non virtuel, qui est l’endroit où ThresholdClass entre en jeu. ThresholdClass spécifie la classe Java qui fournira l’implémentation de la méthode à appeler.

Inscription de méthode

Supposons que nous avons une définition mise à jour ManagedAdder qui remplace la Adder.Add méthode :

partial class ManagedAdder : Adder {
    public override int Add (int a, int b) {
        return (a*2) + (b*2);
    }
}

Rappelez-vous que Adder.Add avait un [Register] attribut personnalisé :

[Register ("add", "(II)I", "GetAddHandler")]

Le [Register] constructeur d’attribut personnalisé accepte trois valeurs :

  1. Nom de la méthode Java, "add" dans ce cas.

  2. Signature de type JNI de la méthode, "(II)I" dans ce cas.

  3. Méthode de connecteur , GetAddHandler dans ce cas. Les méthodes de connecteur seront abordées plus loin.

Les deux premiers paramètres permettent au processus de génération ACW de générer une déclaration de méthode pour remplacer la méthode. L’ACW résultant contient certains des codes suivants :

public class ManagedAdder extends mono.android.test.Adder {
    static final String __md_methods;
    static {
        __md_methods = "n_add:(II)I:GetAddHandler\n" +
            "";
        mono.android.Runtime.register (...);
    }
    @Override
    public int add (int p0, int p1) {
        return n_add (p0, p1);
    }
    private native int n_add (int p0, int p1);
    // ...
}

Notez qu’une @Override méthode est déclarée, qui délègue à une n_méthode préfixe du même nom. Cela garantit que lorsque le code Java appelle ManagedAdder.add, ManagedAdder.n_add est appelé, ce qui permet d’exécuter la méthode C# ManagedAdder.Add de substitution.

Ainsi, la question la plus importante : comment est ManagedAdder.n_add branché à ManagedAdder.Add?

Les méthodes Java native sont inscrites auprès du runtime Java (le runtime Android) via la fonction JNI RegisterNatives. RegisterNatives prend un tableau de structures contenant le nom de la méthode Java, la signature de type JNI et un pointeur de fonction à appeler qui suit la convention d’appel JNI. Le pointeur de fonction doit être une fonction qui accepte deux arguments de pointeur suivis des paramètres de méthode. La méthode Java ManagedAdder.n_add doit être implémentée via une fonction qui a le prototype C suivant :

int FunctionName(JNIEnv *env, jobject this, int a, int b)

Xamarin.Android n’expose pas de RegisterNatives méthode. Au lieu de cela, ACW et MCW fournissent ensemble les informations nécessaires à l’appel RegisterNatives: l’ACW contient le nom de la méthode et la signature de type JNI. Il ne manque qu’un pointeur de fonction pour le raccordement.

C’est là qu’intervient la méthode de connecteur . Le troisième [Register] paramètre d’attribut personnalisé est le nom d’une méthode définie dans le type inscrit ou une classe de base du type inscrit qui n’accepte aucun paramètre et retourne un System.Delegate. Le retourné System.Delegate à son tour fait référence à une méthode qui a la signature de fonction JNI correcte. Enfin, le délégué retourné par la méthode de connecteur doit être rooté afin que le gc ne le collecte pas, car le délégué est fourni à Java.

#pragma warning disable 0169
static Delegate cb_add;
// This method must match the third parameter of the [Register]
// custom attribute, must be static, must return System.Delegate,
// and must accept no parameters.
static Delegate GetAddHandler ()
{
    if (cb_add == null)
        cb_add = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, int, int, int>) n_Add);
    return cb_add;
}
// This method is registered with JNI.
static int n_Add (IntPtr jnienv, IntPtr lrefThis, int a, int b)
{
    Adder __this = Java.Lang.Object.GetObject<Adder>(lrefThis, JniHandleOwnership.DoNotTransfer);
    return __this.Add (a, b);
}
#pragma warning restore 0169

La GetAddHandler méthode crée un Func<IntPtr, IntPtr, int, int, int> délégué qui fait référence à la n_Add méthode, puis appelle JNINativeWrapper.CreateDelegate. JNINativeWrapper.CreateDelegate encapsule la méthode fournie dans un bloc try/catch, afin que toutes les exceptions non gérées soient gérées et entraînent le déclenchement de l’événement AndroidEvent.UnhandledExceptionRaiser . Le délégué résultant est stocké dans la variable statique cb_add afin que le gc ne libère pas le délégué.

Enfin, la n_Add méthode est chargée de marshaler les paramètres JNI sur les types managés correspondants, puis de déléguer l’appel de méthode.

Remarque : Utilisez toujours JniHandleOwnership.DoNotTransfer lors de l’obtention d’un MCW sur un instance Java. Le fait de les traiter comme une référence locale (et donc d’appeler JNIEnv.DeleteLocalRef) interrompt les transitions de pile managée -> Java -> managée.

Terminer la liaison de adder

La liaison managée complète pour le mono.android.tests.Adder type est la suivante :

[Register ("mono/android/test/Adder", DoNotGenerateAcw=true)]
public class Adder : Java.Lang.Object {

    static IntPtr class_ref = JNIEnv.FindClass ("mono/android/test/Adder");

    public Adder ()
    {
    }

    public Adder (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
    }

    protected override Type ThresholdType {
        get {return typeof (Adder);}
    }

    protected override IntPtr ThresholdClass {
        get {return class_ref;}
    }

#region Add
    static IntPtr id_add;

    [Register ("add", "(II)I", "GetAddHandler")]
    public virtual int Add (int a, int b)
    {
        if (id_add == IntPtr.Zero)
            id_add = JNIEnv.GetMethodID (class_ref, "add", "(II)I");
        if (GetType () == ThresholdType)
            return JNIEnv.CallIntMethod (Handle, id_add, new JValue (a), new JValue (b));
        return JNIEnv.CallNonvirtualIntMethod (Handle, ThresholdClass, id_add, new JValue (a), new JValue (b));
    }

#pragma warning disable 0169
    static Delegate cb_add;
    static Delegate GetAddHandler ()
    {
        if (cb_add == null)
            cb_add = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, int, int, int>) n_Add);
        return cb_add;
    }

    static int n_Add (IntPtr jnienv, IntPtr lrefThis, int a, int b)
    {
        Adder __this = Java.Lang.Object.GetObject<Adder>(lrefThis, JniHandleOwnership.DoNotTransfer);
        return __this.Add (a, b);
    }
#pragma warning restore 0169
#endregion
}

Restrictions

Lors de l’écriture d’un type qui correspond aux critères suivants :

  1. Sous-classes Java.Lang.Object

  2. A un [Register] attribut personnalisé

  3. RegisterAttribute.DoNotGenerateAcw est true

Ensuite, pour l’interaction GC, le type ne doit pas avoir de champs pouvant faire référence à une Java.Lang.Object sous-classe ou Java.Lang.Object au moment de l’exécution. Par exemple, les champs de type System.Object et tout type d’interface ne sont pas autorisés. Les types qui ne peuvent pas faire référence à Java.Lang.Object des instances sont autorisés, tels que System.String et List<int>. Cette restriction vise à empêcher la collecte d’objets prématuré par le GC.

Si le type doit contenir un champ instance qui peut faire référence à un Java.Lang.Object instance, le type de champ doit être System.WeakReference ou GCHandle.

Méthodes abstraites de liaison

Les méthodes de liaison abstract sont en grande partie identiques aux méthodes virtuelles de liaison. Il n’y a que deux différences :

  1. La méthode abstraite est abstraite. Il conserve toujours l’attribut [Register] et l’inscription de méthode associée, la liaison de méthode est simplement déplacée vers le Invoker type.

  2. Un type non-est abstractInvoker créé qui sous-classe le type abstrait. Le Invoker type doit remplacer toutes les méthodes abstraites déclarées dans la classe de base, et l’implémentation substituée est l’implémentation de liaison de méthode, bien que le cas de répartition non virtuel puisse être ignoré.

Par exemple, supposons que la méthode ci-dessus mono.android.test.Adder.add était abstract. La liaison C# change de sorte qu’elles Adder.Add soient abstraites, et un nouveau AdderInvoker type est défini qui implémente Adder.Add:

partial class Adder {
    [Register ("add", "(II)I", "GetAddHandler")]
    public abstract int Add (int a, int b);

    // The Method Registration machinery is identical to the
    // virtual method case...
}

partial class AdderInvoker : Adder {
    public AdderInvoker (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
    }

    static IntPtr id_add;
    public override int Add (int a, int b)
    {
        if (id_add == IntPtr.Zero)
            id_add = JNIEnv.GetMethodID (class_ref, "add", "(II)I");
        return JNIEnv.CallIntMethod (Handle, id_add, new JValue (a), new JValue (b));
    }
}

Le Invoker type n’est nécessaire que lors de l’obtention de références JNI à des instances créées par Java.

Interfaces de liaison

Les interfaces de liaison sont conceptuellement similaires aux classes de liaison contenant des méthodes virtuelles, mais de nombreuses spécificités diffèrent de manière subtile (et pas si subtile). Considérez la déclaration d’interface Java suivante :

public interface Progress {
    void onAdd(int[] values, int currentIndex, int currentSum);
}

Les liaisons d’interface ont deux parties : la définition d’interface C# et une définition d’invoker pour l’interface.

Définition d'interface

La définition de l’interface C# doit répondre aux exigences suivantes :

  • La définition de l’interface doit avoir un [Register] attribut personnalisé.

  • La définition d’interface doit étendre le IJavaObject interface. Si vous ne le faites pas, les AW n’héritent pas de l’interface Java.

  • Chaque méthode d’interface doit contenir un [Register] attribut spécifiant le nom de la méthode Java correspondant, la signature JNI et la méthode de connecteur.

  • La méthode de connecteur doit également spécifier le type sur lequel la méthode de connecteur peut se trouver.

Lors de la liaison abstract et virtual des méthodes, la méthode de connecteur est recherchée dans la hiérarchie d’héritage du type inscrit. Les interfaces ne pouvant pas avoir de méthodes contenant des corps, cela ne fonctionne pas, d’où l’exigence de spécifier un type indiquant l’emplacement de la méthode de connecteur. Le type est spécifié dans la chaîne de méthode du connecteur, après un signe deux-points ':', et doit être le nom de type qualifié d’assembly du type contenant l’appelant.

Les déclarations de méthode d’interface sont une traduction de la méthode Java correspondante à l’aide de types compatibles . Pour les types java intégrés, les types compatibles sont les types C# correspondants, par exemple, Java int est C# int. Pour les types de référence, le type compatible est un type qui peut fournir un handle JNI du type Java approprié.

Les membres de l’interface ne seront pas appelés directement par Java . L’appel sera médiatisé par le biais du type Invoker, de sorte qu’une certaine flexibilité est autorisée.

L’interface Java Progress peut être déclarée en C# comme suit :

[Register ("mono/android/test/Adder$Progress", DoNotGenerateAcw=true)]
public interface IAdderProgress : IJavaObject {
    [Register ("onAdd", "([III)V",
            "GetOnAddHandler:Mono.Samples.SanityTests.IAdderProgressInvoker, SanityTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
    void OnAdd (JavaArray<int> values, int currentIndex, int currentSum);
}

Notez dans le ci-dessus que nous mapper le paramètre Java int[] à un int> JavaArray<. Cela n’est pas nécessaire : nous aurions pu le lier à un C# int[], ou à un IList<int>, ou à quelque chose d’autre entièrement. Quel que soit le type choisi, le Invoker doit être en mesure de le traduire en un type Java int[] pour l’appel.

Définition de l’appelant

La Invoker définition de type doit hériter Java.Lang.Object, implémenter l’interface appropriée et fournir toutes les méthodes de connexion référencées dans la définition d’interface. Il existe une autre suggestion qui diffère d’une liaison de classe : les class_ref ID de champ et de méthode doivent être instance membres, et non des membres statiques.

La raison de préférer instance membres est liée JNIEnv.GetMethodID au comportement dans le runtime Android. (Il peut également s’agir d’un comportement Java ; il n’a pas été testé.) JNIEnv.GetMethodID retourne null lors de la recherche d’une méthode qui provient d’une interface implémentée et non de l’interface déclarée. Considérez l’interface Java java.util.SortedMap<K, V> , qui implémente l’interface java.util.Map<K, V> . Map fournit une méthode claire , donc une définition apparemment raisonnable Invoker pour SortedMap serait :

// Fails at runtime. DO NOT FOLLOW
partial class ISortedMapInvoker : Java.Lang.Object, ISortedMap {
    static IntPtr class_ref = JNIEnv.FindClass ("java/util/SortedMap");
    static IntPtr id_clear;
    public void Clear()
    {
        if (id_clear == IntPtr.Zero)
            id_clear = JNIEnv.GetMethodID(class_ref, "clear", "()V");
        JNIEnv.CallVoidMethod(Handle, id_clear);
    }
     // ...
}

L’opération ci-dessus échoue, car JNIEnv.GetMethodID elle retourne null lors de la recherche de la Map.clear méthode via la SortedMap classe instance.

Il existe deux solutions : suivre l’interface d’où provient chaque méthode et avoir un class_ref pour chaque interface, ou conserver tout comme instance membres et effectuer la recherche de méthode sur le type de classe le plus dérivé, et non sur le type d’interface. Ce dernier est effectué dans Mono.Android.dll.

La définition invoker comporte six sections : le constructeur, la méthode, les Dispose membres etThresholdClass, la ThresholdType méthode, l’implémentation de la GetObject méthode d’interface et l’implémentation de la méthode de connecteur.

Constructeur

Le constructeur doit rechercher la classe runtime du instance appelé et stocker la classe runtime dans le champ instance class_ref :

partial class IAdderProgressInvoker {
    IntPtr class_ref;
    public IAdderProgressInvoker (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
        IntPtr lref = JNIEnv.GetObjectClass (Handle);
        class_ref   = JNIEnv.NewGlobalRef (lref);
        JNIEnv.DeleteLocalRef (lref);
    }
}

Remarque : La Handle propriété doit être utilisée dans le corps du constructeur, et non dans le handle paramètre, car sur Android v4.0, le handle paramètre peut être non valide une fois l’exécution du constructeur de base terminée.

Dispose, méthode

La Dispose méthode doit libérer la référence globale allouée dans le constructeur :

partial class IAdderProgressInvoker {
    protected override void Dispose (bool disposing)
    {
        if (this.class_ref != IntPtr.Zero)
            JNIEnv.DeleteGlobalRef (this.class_ref);
        this.class_ref = IntPtr.Zero;
        base.Dispose (disposing);
    }
}

ThresholdType et ThresholdClass

Les ThresholdType membres et ThresholdClass sont identiques à ce qui se trouve dans une liaison de classe :

partial class IAdderProgressInvoker {
    protected override Type ThresholdType {
        get {
            return typeof (IAdderProgressInvoker);
        }
    }
    protected override IntPtr ThresholdClass {
        get {
            return class_ref;
        }
    }
}

GetObject, méthode

Une méthode statique GetObject est requise pour prendre en charge Extensions.JavaCast<T>() :

partial class IAdderProgressInvoker {
    public static IAdderProgress GetObject (IntPtr handle, JniHandleOwnership transfer)
    {
        return new IAdderProgressInvoker (handle, transfer);
    }
}

Méthodes d'interface

Chaque méthode de l’interface doit avoir une implémentation, qui appelle la méthode Java correspondante via JNI :

partial class IAdderProgressInvoker {
    IntPtr id_onAdd;
    public void OnAdd (JavaArray<int> values, int currentIndex, int currentSum)
    {
        if (id_onAdd == IntPtr.Zero)
            id_onAdd = JNIEnv.GetMethodID (class_ref, "onAdd", "([III)V");
        JNIEnv.CallVoidMethod (Handle, id_onAdd, new JValue (JNIEnv.ToJniHandle (values)), new JValue (currentIndex), new JValue (currentSum));
    }
}

Méthodes de connecteur

Les méthodes de connecteur et l’infrastructure de prise en charge sont chargées de marshaler les paramètres JNI sur les types C# appropriés. Le paramètre Java int[] sera passé en tant que JNI jintArray, qui est un IntPtr dans C#. Le IntPtr doit être marshalé sur un JavaArray<int> afin de prendre en charge l’appel de l’interface C# :

partial class IAdderProgressInvoker {
    static Delegate cb_onAdd;
    static Delegate GetOnAddHandler ()
    {
        if (cb_onAdd == null)
            cb_onAdd = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr, IntPtr, int, int>) n_OnAdd);
        return cb_onAdd;
    }

    static void n_OnAdd (IntPtr jnienv, IntPtr lrefThis, IntPtr values, int currentIndex, int currentSum)
    {
        IAdderProgress __this = Java.Lang.Object.GetObject<IAdderProgress>(lrefThis, JniHandleOwnership.DoNotTransfer);
        using (var _values = new JavaArray<int>(values, JniHandleOwnership.DoNotTransfer)) {
            __this.OnAdd (_values, currentIndex, currentSum);
        }
    }
}

Si int[] est préféré à JavaList<int>, JNIEnv.GetArray() peut être utilisé à la place :

int[] _values = (int[]) JNIEnv.GetArray(values, JniHandleOwnership.DoNotTransfer, typeof (int));

Notez, toutefois, que JNIEnv.GetArray copie l’ensemble du tableau entre les machines virtuelles. Pour les grands tableaux, cela peut entraîner une forte pression GC supplémentaire.

Définition complète de l’appelant

Définition IAdderProgressInvoker complète :

class IAdderProgressInvoker : Java.Lang.Object, IAdderProgress {

    IntPtr class_ref;

    public IAdderProgressInvoker (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
        IntPtr lref = JNIEnv.GetObjectClass (Handle);
        class_ref = JNIEnv.NewGlobalRef (lref);
        JNIEnv.DeleteLocalRef (lref);
    }

    protected override void Dispose (bool disposing)
    {
        if (this.class_ref != IntPtr.Zero)
            JNIEnv.DeleteGlobalRef (this.class_ref);
        this.class_ref = IntPtr.Zero;
        base.Dispose (disposing);
    }

    protected override Type ThresholdType {
        get {return typeof (IAdderProgressInvoker);}
    }

    protected override IntPtr ThresholdClass {
        get {return class_ref;}
    }

    public static IAdderProgress GetObject (IntPtr handle, JniHandleOwnership transfer)
    {
        return new IAdderProgressInvoker (handle, transfer);
    }

#region OnAdd
    IntPtr id_onAdd;
    public void OnAdd (JavaArray<int> values, int currentIndex, int currentSum)
    {
        if (id_onAdd == IntPtr.Zero)
            id_onAdd = JNIEnv.GetMethodID (class_ref, "onAdd",
                    "([III)V");
        JNIEnv.CallVoidMethod (Handle, id_onAdd,
                new JValue (JNIEnv.ToJniHandle (values)),
                new JValue (currentIndex),
new JValue (currentSum));
    }

#pragma warning disable 0169
    static Delegate cb_onAdd;
    static Delegate GetOnAddHandler ()
    {
        if (cb_onAdd == null)
            cb_onAdd = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr, IntPtr, int, int>) n_OnAdd);
        return cb_onAdd;
    }

    static void n_OnAdd (IntPtr jnienv, IntPtr lrefThis, IntPtr values, int currentIndex, int currentSum)
    {
        IAdderProgress __this = Java.Lang.Object.GetObject<IAdderProgress>(lrefThis, JniHandleOwnership.DoNotTransfer);
        using (var _values = new JavaArray<int>(values, JniHandleOwnership.DoNotTransfer)) {
            __this.OnAdd (_values, currentIndex, currentSum);
        }
    }
#pragma warning restore 0169
#endregion
}

Références d’objets JNI

De nombreuses méthodes JNIEnv retournent des références d’objetsJNI, qui sont similaires à GCHandles. JNI fournit trois types différents de références d’objet : les références locales, les références globales et les références globales faibles. Tous les trois sont représentés sous la forme System.IntPtr, mais (conformément à la section Types de fonctions JNI) les s retournés à partir de JNIEnv méthodes ne sont pas tous IntPtrdes références. Par exemple, JNIEnv.GetMethodID retourne un IntPtr, mais il ne retourne pas de référence d’objet, il retourne un jmethodID. Pour plus d’informations, consultez la documentation de la fonction JNI .

Les références locales sont créées par la plupart des méthodes de création de références. Android autorise uniquement un nombre limité de références locales à un moment donné, généralement 512. Les références locales peuvent être supprimées via JNIEnv.DeleteLocalRef. Contrairement à JNI, toutes les méthodes JNIEnv de référence qui retournent des références d’objet retournent des références locales ; JNIEnv.FindClass retourne une référence globale . Il est vivement recommandé de supprimer les références locales aussi rapidement que possible, éventuellement en construisant un Objet Java.Lang.Object autour de l’objet et en spécifiant JniHandleOwnership.TransferLocalRef au constructeur Java.Lang.Object(IntPtr handle, JniHandleOwnership transfer).

Les références globales sont créées par JNIEnv.NewGlobalRef et JNIEnv.FindClass. Ils peuvent être détruits avec JNIEnv.DeleteGlobalRef. Les émulateurs ont une limite de 2 000 références globales en suspens, tandis que les appareils matériels ont une limite d’environ 52 000 références globales.

Les références globales faibles sont uniquement disponibles sur Android v2.2 (Froyo) et versions ultérieures. Les références globales faibles peuvent être supprimées avec JNIEnv.DeleteWeakGlobalRef.

Gestion des références locales JNI

Les méthodes JNIEnv.GetObjectField, JNIEnv.GetStaticObjectField, JNIEnv.CallObjectMethod, JNIEnv.CallNonvirtualObjectMethod et JNIEnv.CallStaticObjectMethod retournent un IntPtr qui contient une référence locale JNI à un objet Java, ou IntPtr.Zero si Java a retourné null. En raison du nombre limité de références locales qui peuvent être en attente à la fois (512 entrées), il est souhaitable de s’assurer que les références sont supprimées en temps voulu. Il existe trois façons de traiter les références locales : les supprimer explicitement, créer un Java.Lang.Object instance pour les contenir et utiliser Java.Lang.Object.GetObject<T>() pour créer un wrapper d’appel managé autour d’elles.

Suppression explicite des références locales

JNIEnv.DeleteLocalRef est utilisé pour supprimer des références locales. Une fois que la référence locale a été supprimée, elle ne peut plus être utilisée. Il faut donc veiller à ce que JNIEnv.DeleteLocalRef soit la dernière chose effectuée avec la référence locale.

IntPtr lref = JNIEnv.CallObjectMethod(instance, methodID);
try {
    // Do something with `lref`
}
finally {
    JNIEnv.DeleteLocalRef (lref);
}

Encapsulage avec Java.Lang.Object

Java.Lang.Object fournit un constructeur Java.Lang.Object(IntPtr handle, JniHandleOwnership transfer) qui peut être utilisé pour encapsuler une référence JNI sortante. Le paramètre JniHandleOwnership détermine la façon dont le IntPtr paramètre doit être traité :

  • JniHandleOwnership.DoNotTransfer : le instance créé Java.Lang.Object crée une référence globale à partir du handle paramètre et handle reste inchangé. L’appelant est responsable de libérer handle , si nécessaire.

  • JniHandleOwnership.TransferLocalRef : le instance créé Java.Lang.Object crée une référence globale à partir du handle paramètre et handle est supprimé avec JNIEnv.DeleteLocalRef . L’appelant ne doit pas libérer handle et ne doit pas utiliser handle une fois l’exécution du constructeur terminée.

  • JniHandleOwnership.TransferGlobalRef : le instance créé Java.Lang.Object prend la propriété du handle paramètre. L’appelant ne doit pas libérer handle .

Étant donné que les méthodes d’appel de méthode JNI retournent des références locales, JniHandleOwnership.TransferLocalRef sont normalement utilisées :

IntPtr lref = JNIEnv.CallObjectMethod(instance, methodID);
var value = new Java.Lang.Object (lref, JniHandleOwnership.TransferLocalRef);

La référence globale créée n’est pas libérée tant que le Java.Lang.Object instance n’est pas ramassé. Si vous le pouvez, l’élimination de l’instance libérera la référence globale, ce qui accélère le garbage collection :

IntPtr lref = JNIEnv.CallObjectMethod(instance, methodID);
using (var value = new Java.Lang.Object (lref, JniHandleOwnership.TransferLocalRef)) {
    // use value ...
}

Utilisation de Java.Lang.Object.GetObject<T>()

Java.Lang.Object fournit une méthode Java.Lang.Object.GetObject<T>(IntPtr handle, JniHandleOwnership transfer) qui peut être utilisée pour créer un wrapper callable managé du type spécifié.

Le type T doit répondre aux exigences suivantes :

  1. T doit être un type de référence.

  2. T doit implémenter l'interface IJavaObject.

  3. Si T n’est pas une classe ou une interface abstraite, vous T devez fournir à un constructeur les types (IntPtr, JniHandleOwnership) de paramètres .

  4. S’il s’agit T d’une classe abstraite ou d’une interface, un appelantdoit être disponible pour T . Un appelant est un type non abstrait qui hérite ou implémente TT , et a le même nom qu’avec T un suffixe Invoker. Par exemple, si T est l’interface Java.Lang.IRunnable , le type Java.Lang.IRunnableInvoker doit exister et doit contenir le constructeur requis (IntPtr, JniHandleOwnership) .

Étant donné que les méthodes d’appel de méthode JNI retournent des références locales, JniHandleOwnership.TransferLocalRef sont normalement utilisées :

IntPtr lrefString = JNIEnv.CallObjectMethod(instance, methodID);
Java.Lang.String value = Java.Lang.Object.GetObject<Java.Lang.String>( lrefString, JniHandleOwnership.TransferLocalRef);

Recherche de types Java

Pour rechercher un champ ou une méthode dans JNI, le type de déclaration du champ ou de la méthode doit d’abord être recherché. La méthode Android.Runtime.JNIEnv.FindClass(string)) est utilisée pour rechercher des types Java. Le paramètre string est la référence de type simplifié ou la référence de type complet pour le type Java. Pour plus d’informations sur les références de type simplifiées et complètes, consultez la section Références de type JNI .

Remarque : Contrairement à toute autre JNIEnv méthode qui retourne des instances d’objet, FindClass retourne une référence globale, et non une référence locale.

Champs d’instance

Les champs sont manipulés via des ID de champ. Les ID de champ sont obtenus via JNIEnv.GetFieldID, ce qui nécessite la classe dans laquelle le champ est défini, le nom du champ et la signature de type JNI du champ.

Les ID de champ n’ont pas besoin d’être libérés et sont valides tant que le type Java correspondant est chargé. (Android ne prend actuellement pas en charge le déchargement de classe.)

Il existe deux ensembles de méthodes pour manipuler des champs instance : l’un pour lire des champs instance et l’autre pour écrire des champs instance. Tous les jeux de méthodes nécessitent un ID de champ pour lire ou écrire la valeur du champ.

Lecture des valeurs de champ d’instance

L’ensemble de méthodes permettant de lire instance valeurs de champ suit le modèle de nommage :

* JNIEnv.Get*Field(IntPtr instance, IntPtr fieldID);

* est le type du champ :

Écriture de valeurs de champ d’instance

L’ensemble de méthodes permettant d’écrire instance valeurs de champ suit le modèle de nommage :

JNIEnv.SetField(IntPtr instance, IntPtr fieldID, Type value);

Type est le type du champ :

  • JNIEnv.SetField) : écrivez la valeur d’un champ qui n’est pas un type intégré, tel que java.lang.Object , les tableaux et les types d’interface. La IntPtr valeur peut être une référence locale JNI, une référence globale JNI, une référence globale faible JNI ou IntPtr.Zero (pour null ).

  • JNIEnv.SetField) : écrivez la valeur de bool instance champs.

  • JNIEnv.SetField) : écrivez la valeur de sbyte instance champs.

  • JNIEnv.SetField) : écrivez la valeur de char instance champs.

  • JNIEnv.SetField) : écrivez la valeur de short instance champs.

  • JNIEnv.SetField) : écrivez la valeur de int instance champs.

  • JNIEnv.SetField) : écrivez la valeur de long instance champs.

  • JNIEnv.SetField) : écrivez la valeur de float instance champs.

  • JNIEnv.SetField) : écrivez la valeur de double instance champs.

Champs statiques

Les champs statiques sont manipulés via des ID de champ. Les ID de champ sont obtenus via JNIEnv.GetStaticFieldID, ce qui nécessite la classe dans laquelle le champ est défini, le nom du champ et la signature de type JNI du champ.

Les ID de champ n’ont pas besoin d’être libérés et sont valides tant que le type Java correspondant est chargé. (Android ne prend actuellement pas en charge le déchargement de classe.)

Il existe deux ensembles de méthodes pour manipuler des champs statiques : l’un pour lire des champs instance et l’autre pour écrire des champs instance. Tous les jeux de méthodes nécessitent un ID de champ pour lire ou écrire la valeur du champ.

Lecture des valeurs de champ statiques

L’ensemble de méthodes de lecture des valeurs de champ statiques suit le modèle de nommage :

* JNIEnv.GetStatic*Field(IntPtr class, IntPtr fieldID);

* est le type du champ :

Écriture de valeurs de champ statiques

L’ensemble de méthodes permettant d’écrire des valeurs de champ statiques suit le modèle de nommage :

JNIEnv.SetStaticField(IntPtr class, IntPtr fieldID, Type value);

Type est le type du champ :

Méthodes d’instance

Les méthodes d’instance sont appelées via des ID de méthode. Les ID de méthode sont obtenus via JNIEnv.GetMethodID, ce qui nécessite le type dans lequel la méthode est définie, le nom de la méthode et la signature de type JNI de la méthode.

Les ID de méthode n’ont pas besoin d’être libérés et sont valides tant que le type Java correspondant est chargé. (Android ne prend actuellement pas en charge le déchargement de classe.)

Il existe deux ensembles de méthodes pour appeler des méthodes : l’un pour appeler des méthodes virtuellement et l’autre pour appeler des méthodes non virtuelles. Les deux ensembles de méthodes nécessitent un ID de méthode pour appeler la méthode, et l’appel non virtuel nécessite également que vous spécifiiez l’implémentation de classe à appeler.

Les méthodes d’interface ne peuvent être recherchées que dans le type de déclaration ; les méthodes qui proviennent d’interfaces étendues/héritées ne peuvent pas être recherchées. Pour plus d’informations, consultez la section Interfaces de liaison /Implémentation de l’invoker ultérieure.

Toute méthode déclarée dans la classe ou toute classe de base ou interface implémentée peut être recherchée.

Appel de méthode virtuelle

L’ensemble de méthodes d’appel de méthodes suit pratiquement le modèle de nommage :

* JNIEnv.Call*Method( IntPtr instance, IntPtr methodID, params JValue[] args );

* est le type de retour de la méthode.

Appel de méthode non virtuelle

L’ensemble de méthodes d’appel de méthodes non virtuelles suit le modèle de nommage :

* JNIEnv.CallNonvirtual*Method( IntPtr instance, IntPtr class, IntPtr methodID, params JValue[] args );

* est le type de retour de la méthode. L’appel de méthode non virtuelle est généralement utilisé pour appeler la méthode de base d’une méthode virtuelle.

Méthodes statiques

Les méthodes statiques sont appelées via des ID de méthode. Les ID de méthode sont obtenus via JNIEnv.GetStaticMethodID, ce qui nécessite le type dans lequel la méthode est définie, le nom de la méthode et la signature de type JNI de la méthode.

Les ID de méthode n’ont pas besoin d’être libérés et sont valides tant que le type Java correspondant est chargé. (Android ne prend actuellement pas en charge le déchargement de classe.)

Appel de méthode statique

L’ensemble de méthodes d’appel de méthodes suit pratiquement le modèle de nommage :

* JNIEnv.CallStatic*Method( IntPtr class, IntPtr methodID, params JValue[] args );

* est le type de retour de la méthode.

JNI Type Signatures

Les signatures de type JNI sont des références de type JNI (mais pas des références de type simplifiées), à l’exception des méthodes. Avec les méthodes, la signature de type JNI est une parenthèse '('ouverte , suivie des références de type pour tous les types de paramètres concaténés ensemble (sans virgules de séparation ni autre chose), suivie d’une parenthèse ')'fermante , suivie de la référence de type JNI du type de retour de méthode.

Par exemple, compte tenu de la méthode Java :

long f(int n, String s, int[] array);

La signature de type JNI est :

(ILjava/lang/String;[I)J

En général, il est fortement recommandé d’utiliser la javap commande pour déterminer les signatures JNI. Par exemple, la signature de type JNI de la méthode java.lang.Thread.State.valueOf(String) est « (Ljava/lang/String;)Ljava/lang/Thread$State; », tandis que la signature de type JNI de la méthode java.lang.Thread.State.values est « ()[Ljava/lang/Thread$State; ». Attention aux points-virgules de fin; elles font partie de la signature de type JNI.

Références de type JNI

Les références de type JNI sont différentes des références de type Java. Vous ne pouvez pas utiliser de noms de types Java complets, par java.lang.String exemple avec JNI. Vous devez utiliser les variantes "java/lang/String" JNI ou "Ljava/lang/String;", en fonction du contexte ; voir ci-dessous pour plus d’informations. Il existe quatre types de références de type JNI :

  • intégré
  • Simplifié
  • type
  • array

Références de type intégrées

Les références de type intégrées sont un caractère unique, utilisé pour référencer des types de valeurs intégrées. Le mappage est le suivant :

  • "B" pour sbyte .
  • "S" pour short .
  • "I" pour int .
  • "J" pour long .
  • "F" pour float .
  • "D" pour double .
  • "C" pour char .
  • "Z" pour bool .
  • "V" pour les types de retour de void méthode.

Références de type simplifiées

Les références de type simplifiées ne peuvent être utilisées que dans JNIEnv.FindClass(string)). Il existe deux façons de dériver une référence de type simplifié :

  1. À partir d’un nom Java complet, remplacez chaque '.' dans le nom du package et avant le nom de type par '/' , et chaque '.' dans un nom de type par '$' .

  2. Lisez la sortie de 'unzip -l android.jar | grep JavaName' .

L’un des deux entraîne le mappage du type Java java.lang.Thread.State à la référence java/lang/Thread$Statede type simplifié .

Références de type

Une référence de type est une référence de type intégrée ou une référence de type simplifiée avec un 'L' préfixe et un ';' suffixe. Pour le type Java java.lang.String, la référence de type simplifiée est "java/lang/String", tandis que la référence de type est "Ljava/lang/String;".

Les références de type sont utilisées avec les références de type array et avec les signatures JNI.

Un autre moyen d’obtenir une référence de type consiste à lire la sortie de 'javap -s -classpath android.jar fully.qualified.Java.Name'. Selon le type impliqué, vous pouvez utiliser une déclaration de constructeur ou un type de retour de méthode pour déterminer le nom JNI. Par exemple :

$ javap -classpath android.jar -s java.lang.Thread.State
Compiled from "Thread.java"
public final class java.lang.Thread$State extends java.lang.Enum{
public static final java.lang.Thread$State NEW;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State RUNNABLE;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State BLOCKED;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State WAITING;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State TIMED_WAITING;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State TERMINATED;
  Signature: Ljava/lang/Thread$State;
public static java.lang.Thread$State[] values();
  Signature: ()[Ljava/lang/Thread$State;
public static java.lang.Thread$State valueOf(java.lang.String);
  Signature: (Ljava/lang/String;)Ljava/lang/Thread$State;
static {};
  Signature: ()V
}

Thread.State étant un type d’énumération Java, nous pouvons utiliser la signature de la valueOf méthode pour déterminer que la référence de type est Ljava/lang/Thread$State;.

Références de type de tableau

Les références de type tableau sont '[' précédées d’une référence de type JNI. Les références de type simplifiées ne peuvent pas être utilisées lors de la spécification de tableaux.

Par exemple, int[] est "[I", int[][] est "[[I"et java.lang.Object[] est "[Ljava/lang/Object;".

Génériques Java et effacement de type

La plupart du temps, comme le montre JNI, les génériques Java n’existent pas. Il y a quelques « rides », mais ces rides sont dans la façon dont Java interagit avec les génériques, pas avec la façon dont JNI recherche et appelle les membres génériques.

Il n’existe aucune différence entre un type ou un membre générique et un type ou membre non générique lors de l’interaction via JNI. Par exemple, le type générique java.lang.Class<T> est également le type java.lang.Classgénérique « brut », qui ont tous deux la même référence de type simplifié, "java/lang/Class".

Prise en charge de l’interface java native

Android.Runtime.JNIEnv est un wrapper managé pour jave Native Interface (JNI). Les fonctions JNI sont déclarées dans la spécification d’interface native Java, bien que les méthodes aient été modifiées pour supprimer le paramètre explicite JNIEnv* et IntPtr soient utilisées à la place de jobject, jclass, jmethodIDetc. Par exemple, considérez la fonction NewObject JNI :

jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);

Ceci est exposé en tant que méthode JNIEnv.NewObject :

public static IntPtr NewObject(IntPtr clazz, IntPtr jmethod, params JValue[] parms);

La traduction entre les deux appels est relativement simple. En C, vous auriez :

jobject CreateMapActivity(JNIEnv *env)
{
    jclass    Map_Class   = (*env)->FindClass(env, "mono/samples/googlemaps/MyMapActivity");
    jmethodID Map_defCtor = (*env)->GetMethodID (env, Map_Class, "<init>", "()V");
    jobject   instance    = (*env)->NewObject (env, Map_Class, Map_defCtor);

    return instance;
}

L’équivalent C# serait :

IntPtr CreateMapActivity()
{
    IntPtr Map_Class   = JNIEnv.FindClass ("mono/samples/googlemaps/MyMapActivity");
    IntPtr Map_defCtor = JNIEnv.GetMethodID (Map_Class, "<init>", "()V");
    IntPtr instance    = JNIEnv.NewObject (Map_Class, Map_defCtor);

    return instance;
}

Une fois que vous avez un objet Java instance conservé dans un IntPtr, vous voudrez probablement en faire quelque chose. Vous pouvez utiliser des méthodes JNIEnv telles que JNIEnv.CallVoidMethod() pour ce faire, mais s’il existe déjà un wrapper C# analogique, vous voudrez construire un wrapper sur la référence JNI. Vous pouvez le faire via la méthode d’extension T> Extensions.JavaCast< :

IntPtr lrefActivity = CreateMapActivity();

// imagine that Activity were instead an interface or abstract type...
Activity mapActivity = new Java.Lang.Object(lrefActivity, JniHandleOwnership.TransferLocalRef)
    .JavaCast<Activity>();

Vous pouvez également utiliser la méthode T> Java.Lang.Object.GetObject< :

IntPtr lrefActivity = CreateMapActivity();

// imagine that Activity were instead an interface or abstract type...
Activity mapActivity = Java.Lang.Object.GetObject<Activity>(lrefActivity, JniHandleOwnership.TransferLocalRef);

En outre, toutes les fonctions JNI ont été modifiées en supprimant le JNIEnv* paramètre présent dans chaque fonction JNI.

Résumé

Traiter directement avec JNI est une expérience terrible qui doit être évitée à tout prix. Malheureusement, ce n’est pas toujours évitable; Nous espérons que ce guide vous aidera lorsque vous accédez aux cas Java non liés avec Mono pour Android.