Partager via


Utilisation de JNI et Xamarin.Android

Xamarin.Android autorise l’écriture d’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 pouvant être appelé (MCW) managé pour appeler du code Java. Dans de nombreux cas, « inline » JNI 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 méthode unique sur une classe Java que pour 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 les membres non présents dans Mono.Android.dll et les types non présents 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 : il 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 pour prendre en charge 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.
  • Guide pratique pour exposer des méthodes virtuelles pour autoriser la substitution à partir du code managé.
  • Guide pratique pour exposer des interfaces.

Spécifications

JNI, comme 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 pouvant être appelé gérés

Un wrapper à appel managé (MCW) est une liaison pour une classe ou une interface Java qui encapsule toutes les machines JNI afin que le code C# client n’ait pas besoin de s’inquiéter de la complexité sous-jacente de JNI. La plupart d’entre elles se composent de Mono.Android.dll wrappers pouvant être appelé gérés.

Les wrappers pouvant être appelé gérés servent à deux fins :

  1. Encapsulez l’utilisation de JNI afin que le code client n’ait pas besoin de connaître la complexité sous-jacente.
  2. Permettre aux types Java de sous-classe et d’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 JNI « inline » est parfaitement acceptable et est utile pour l’utilisation unique des membres Java non liés. L’implémentation de sous-classes et d’interface nécessite l’utilisation de wrappers pouvant être appelé géré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 d’ART au moment de l’exécution. (Plus précisément, le La fonction DefineClass JNI n’est pas prise en charge par le runtime Android. Les wrappers pouvant être appelé Android constituent donc le manque 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 substitué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 liste d’interface Java que le type managé, implémentant les mêmes constructeurs et déclarant toutes les méthodes de classe de base et d’interface substitué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 peut arriver que vous deviez implémenter une interface Android (par exemple, 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 (wrapper pouvant être appelé Android) pour le type managé donné. Étant donné que monodroid.exe recherche Java.Lang.Object uniquement les sous-classes (qui doivent implémenter IJavaObject), le sous-classement Java.Lang.Object nous fournit un moyen d’implémenter des interfaces dans le 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 des détails d’implémentation susceptibles de changer sans préavis (et est présenté ici uniquement parce que les développeurs peuvent être curieux de ce qui se passe sous le capot).

Par exemple, étant donné 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 Android Callable 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 les déclarations de méthode native sont fournies pour chaque méthode substitué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, l’instance d’affichage gonflé tente de rechercher la méthode Java.

  • L’interface java.io.Serializable nécessite et writeObject des readObject méthodes. É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 de sortie dans le code Java à partir du 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é introduites pour offrir une solution aux scénarios ci-dessus. Les deux attributs résident dans l’espace Java.Interop de noms :

  • ExportAttribute : spécifie un nom de méthode et ses types d’exceptions attendus (pour donner des « levées » explicites en 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 distribution vers l’appel JNI correspondant à la méthode managée. Cela 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 en tant qu’initialiseur de champ. Cela peut être utilisé avec android.os.Parcelable.

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

  • L’empaquetage échoue en raison de Mono.Android.Export.dll manquantes : si vous avez utilisé ExportAttribute ou ExportFieldAttribute sur certaines méthodes de votre code ou 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. Il est distinct de Mono.Android.dll car il ajoute une taille supplémentaire à l’application.

  • Dans la build Release, MissingMethodException se produit pour les méthodes d’exportation : dans la build Release, MissingMethodException elle 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 fournir 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 existante 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, ce cas n’est pas entièrement déterminant. 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, il ExportParameterAttribute doit être utilisé pour donner explicitement au paramètre correspondant ou à la valeur de retour un type.

Attribut d’annotation

Dans Xamarin.Android 4.2, nous avons converti IAnnotation les 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 que les modifications directionnelles suivantes sont apportées :

  • Le générateur de liaison génère Java.Lang.DeprecatedAttribute à partir java.Lang.Deprecated de (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 Java peuvent toujours être utilisés comme objets Java habituels (si cette utilisation existe). Il y aura Deprecated et DeprecatedAttribute des classes.

  • La Java.Lang.DeprecatedAttribute classe est marquée comme [Annotation] . Lorsqu’il existe un attribut personnalisé hérité de cet [Annotation] attribut, la tâche msbuild génère une annotation Java pour cet attribut personnalisé (@Deprecated) dans le wrapper pouvant être appelé 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 conteneur (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 la ExportAttribute méthode pour obtenir 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 certaine annotation, 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 façon 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 sur 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, toute 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 du handle de type

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, il s’agit d’un class_ref champ :

static IntPtr class_ref = JNIEnv.FindClass(CLASS);

Pour plus d’informations sur le jeton, consultez la CLASS section Références de type 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, étant donné que JNI fait la distinction entre les champs statiques et les champs d’instance, différentes méthodes doivent être utilisées lors de l’implémentation des propriétés.

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

  1. Méthode d’ID de champ Get. La méthode get field id est chargée de retourner un handle de champ que la valeur de champ get et définir les méthodes de valeur de champ utilisent. L’obtention de l’ID de champ nécessite la connaissance du type déclarant, du nom du champ et de 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 set. 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 et JNIEnv.GetStatic*FieldJNIEnv.SetStaticField.

Les champs d’instance utilisent les méthodes JNIEnv.GetFieldID et JNIEnv.Get*FieldJNIEnv.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 une System.IO.Stream instance, et nous utilisons JniHandleOwnership.TransferLocalRef parce que JNIEnv.GetStaticObjectField retourne une référence locale.

De nombreux types Android.Runtime ont FromJniHandle des 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 de la méthode à appeler. La méthode get method id est responsable du renvoi d’un handle de méthode que les méthodes d’appel de méthode utiliseront. L’obtention de l’ID de méthode nécessite la connaissance du type déclarant, du nom de la méthode et de la signature de type JNI de la méthode.

  2. Appelez la méthode.

Tout comme avec 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 d’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 JNIEnv.CallNonvirtual*Method de méthodes 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 implémentée (pour les méthodes d’interface). La section Prise en charge de l’héritage, interfaces décrit la complexité des méthodes virtuelles et des méthodes d’interface.

Méthodes statiques

La liaison d’une méthode statique implique l’utilisation JNIEnv.GetStaticMethodID d’un handle de méthode, puis l’utilisation de 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, afin que le handle de méthode n’ait pas besoin d’être recherché sur 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.CallStaticObjectMethod retourne un IntPtr qui contient le handle de l’instance Java retournée. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) est utilisé pour convertir le handle Java en une instance d’objet fortement typée.

Liaison de méthode d’instance non virtuelle

La liaison d’une final méthode d’instance ou d’une méthode d’instance qui ne nécessite pas de substitution implique l’utilisation JNIEnv.GetMethodID d’un handle de méthode, puis l’utilisation de la méthode appropriée JNIEnv.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, afin que le handle de méthode n’ait pas besoin d’être recherché sur 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.CallStaticObjectMethod retourne un IntPtr qui contient le handle de l’instance Java retournée. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) est utilisé pour convertir le handle Java en une instance d’objet fortement typée.

Constructeurs de liaison

Les constructeurs sont des méthodes Java portant le nom "<init>". Tout comme avec les méthodes d’instance Java, 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 est JNIEnv.NewObject 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 la Java.Lang.Objectsous-classification, une sémantique supplémentaire entre en jeu : une Java.Lang.Object instance conserve une référence globale à une instance Java via la Java.Lang.Object.Handle propriété.

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

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

  3. Sinon, le wrapper pouvant être appelé Android (ACW) correspondant est this.GetType instancié par le biais de son constructeur par défaut. Les wrappers pouvant être appelé Android sont générés lors de la création du package pour chaque Java.Lang.Object sous-classe pour laquelle RegisterAttribute.DoNotGenerateAcw la valeur n’est pas définie true.

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

Pour les liaisons de classe, il peut s’agir du comportement correct si le type Java contient un constructeur par défaut et/ou qu’aucun autre constructeur n’a besoin d’être appelé. Sinon, un constructeur doit être fourni pour effectuer 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é a une valeur autre que IntPtr.Zero si un wrapper Android Callable a été construit dans le code Java, et la liaison de classe est construite pour contenir l’instance de wrapper android callable créée. Par exemple, lorsque Android crée une mono.samples.helloworld.HelloAndroid instance, le wrapper pouvant être appelé Android est créé en premier et le constructeur Java HelloAndroid crée une instance du type correspondant Mono.Samples.HelloWorld.HelloAndroid , avec la Object.Handle propriété définie sur l’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, une instance du wrapper appelant Android correspondant doit être créée et utiliser Object.SetHandle pour stocker le handle retourné par JNIEnv.CreateInstance.

  4. Si le type d’exécution actuel est identique au type déclarant, 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 helpers pour effectuer un JNIEnv.FindClass, , JNIEnv.GetMethodIDJNIEnv.NewObjectet JNIEnv.DeleteGlobalReference sur la valeur retournée par JNIEnv.FindClass. Voir la section suivante pour plus de détails.

Prise en charge de l’héritage, des interfaces

La sous-classe d’un type Java ou l’implémentation d’une interface Java nécessite la génération de wrappers pouvant être appelé Android (ACWs) 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 par le biais de 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 différents noms entre Java et C#.

Avant Xamarin.Android 4.0, l’attribut [Register] personnalisé n’était pas disponible pour les types Java existants « alias ». Cela est dû au fait que le processus de génération ACW génère des AW 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 appelé gérés qui ne entraînent pas la génération d’AW au moment de la création du package. Cela permet de lier des types Java existants. Par exemple, considérez la classe Java simple suivante, Adderqui contient une méthode, addqui ajoute à des 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 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-classe le type Java Adder , mais il n’est pas particulièrement intéressant : le type C# Adder ne définit aucune méthode virtuelle, donc ManagedAdder ne peut pas remplacer quoi que ce soit.

Les méthodes de liaison virtual permettant de remplacer 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 distribution de méthode virtuelle ou non virtuelle. Il 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 d’exécution de la méthode Java. Il 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 de la méthode Java à appeler. La id_add valeur est obtenue à partir de JNIEnv.GetMethodID, 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 il est comparé pour ThresholdType déterminer si la répartition virtuelle ou non virtuelle est requise. La répartition virtuelle est requise lorsque des GetType correspondances ThresholdTypesont requises, comme Handle peut faire référence à une sous-classe allouée par Java qui remplace la méthode.

Quand GetType elle ne correspond ThresholdTypepas, Adder a été sous-classée (par exemple, par ManagedAdder), et l’implémentation Adder.Add ne sera appelée que si la sous-classe appelée base.Add. Il s’agit du cas de répartition non virtuel, qui est l’endroit ThresholdClass où vient. 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 qu’il Adder.Add y 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 ultérieurement.

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 un certain nombre du code suivant :

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 sera appelé, ce qui permettra l’exécution de la méthode C# ManagedAdder.Add substituée.

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

Les méthodes Java native sont inscrites auprès du runtime Java (runtime Android) via la fonction RegisterNatives JNI. RegisterNatives prend un tableau de structures contenant le nom de la méthode Java, la signature de type JNI et un pointeur de fonction pour appeler qui suit la convention d’appel JNI. Le pointeur de fonction doit être une fonction qui prend deux arguments de pointeur suivis des paramètres de méthode. La méthode Java ManagedAdder.n_add doit être implémentée par le biais d’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 pour appeler RegisterNatives: l’ACW contient le nom de la méthode et la signature de type JNI, la seule chose manquante est un pointeur de fonction à raccorder.

C’est là que la méthode de connecteur est entrée. 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 retour 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 du 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 l’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 vers les types managés correspondants, puis de déléguer l’appel de méthode.

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

Terminer la liaison de l’éditeur

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. Possède un [Register] attribut personnalisé

  3. RegisterAttribute.DoNotGenerateAcw est true

Ensuite, pour l’interaction GC, le type ne doit pas avoir de champs qui peuvent faire référence à une Java.Lang.Object ou Java.Lang.Object sous-classe 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 est d’empêcher la collecte d’objets prématuré par le GC.

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

Liaison de méthodes abstraites

Les méthodes de liaison sont largement identiques aux méthodes virtuelles de abstract liaison. Il n’existe 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 autre que celui-ci abstract Invoker est 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 la liaison de méthode, bien que la casse de répartition non virtuelle puisse être ignorée.

Par exemple, supposons que la méthode ci-dessus mono.android.test.Adder.add était abstract. La liaison C# change de sorte qu’elle Adder.Add soit abstraite et qu’un nouveau AdderInvoker type soit 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 est nécessaire uniquement lors de l’obtention de références JNI aux 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 la plupart des spécificités diffèrent de manière subtile (et non 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 de l’interface C# et une définition d’appelant pour l’interface.

Définition de l’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 de l’interface doit étendre le IJavaObject interface. L’échec de cette opération empêche les AW d’hériter de l’interface Java.

  • Chaque méthode d’interface doit contenir un [Register] attribut spécifiant le nom de méthode Java correspondant, la signature JNI et la méthode du 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 peuvent pas contenir de méthodes contenant des corps. Cela ne fonctionne donc pas, ce qui exige qu’un type soit spécifié indiquant où se trouve 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 intégrés Java, 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 directement appelés par Java : l’appel sera médiatisé par le biais du type d’appelant. Par conséquent, 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 que nous mappons le paramètre Java à un int> JavaArray<.int[] Cela n’est pas nécessaire : nous pourrions l’avoir lié à un C# int[], ou à un IList<int>, ou quelque chose d’autre entièrement. Quel que soit le type choisi, il Invoker doit être en mesure de le traduire en un type Java int[] pour l’appel.

Définition de l’appelant

La définition de Invoker 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 de l’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 des membres d’instance, et non des membres statiques.

La raison pour laquelle les membres d’instance préférés doivent faire partie JNIEnv.GetMethodID du 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 provenant d’une interface implémentée et non de l’interface déclarée. Considérez l’interface Java.util.SortedMap<K, V> Java, 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 par le biais de l’instance de SortedMap classe.

Il existe deux solutions à ceci : suivre l’interface à partir de laquelle chaque méthode provient, et avoir une class_ref pour chaque interface, ou conserver tout en tant que membres d’instance et effectuer la recherche de méthode sur le type de classe le plus dérivé, et non le type d’interface. Cette dernière est effectuée dans Mono.Android.dll.

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

Constructeur

Le constructeur doit rechercher la classe runtime de l’instance appelée et stocker la classe runtime dans le champ d’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 ne pas être valide après l’exécution du constructeur de base.

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 les membres 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 responsables du marshaling des paramètres JNI aux types C# appropriés. Le paramètre Java int[] est passé en tant que JNI jintArray, qui est un IntPtr élément C#. La IntPtr valeur doit être marshalée JavaArray<int> pour 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[] vous préférez 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’intégralité du tableau entre les machines virtuelles. Par conséquent, pour les grands tableaux, cela peut entraîner une forte pression gc.

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’objet JNI, qui sont similaires à GCHandles. JNI fournit trois types de références d’objet différents : références locales, références globales et références globales faibles. Les trois sont représentés sous System.IntPtrla forme , mais (conformément à la section Types de fonctions JNI) les références ne sont pas toutes retournées par JNIEnv les IntPtrméthodes. Par exemple, JNIEnv.GetMethodID retourne une référence d’objet IntPtr, mais elle ne retourne pas de référence d’objet, elle renvoie 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érence. Android autorise uniquement un nombre limité de références locales à exister à tout moment, 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 le 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 exceptionnelles, tandis que les appareils matériels ont une limite d’environ 52 000 références globales.

Les références globales faibles sont disponibles uniquement 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 une IntPtr référence locale JNI à un objet Java, ou IntPtr.Zero si Java est 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 dont les références locales peuvent être traitées : les supprimer explicitement, créer une Java.Lang.Object instance pour les contenir et l’utiliser Java.Lang.Object.GetObject<T>() pour créer un wrapper pouvant être appelé géré autour d’eux.

Suppression explicite de références locales

JNIEnv.DeleteLocalRef est utilisé pour supprimer des références locales. Une fois la référence locale supprimée, elle ne peut plus être utilisée. Vous devez donc veiller à ce qu’elle 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);
}

Wrapping 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 : l’instance créée Java.Lang.Object crée une référence globale à partir du handle paramètre et handle n’est pas modifiée. L’appelant est chargé de libérer handle , si nécessaire.

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

  • JniHandleOwnership.TransferGlobalRef : l’instance créée Java.Lang.Object prend la propriété du handle paramètre. L’appelant ne doit pas être libre handle .

Étant donné que les méthodes d’appel de méthode JNI retournent des références locales, JniHandleOwnership.TransferLocalRef elles 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 ne sera pas libérée tant que l’instance Java.Lang.Object n’est pas garbage collected. Si vous êtes en mesure de supprimer l’instance, la suppression de l’instance libère la référence globale, accélérant les garbage collections :

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 pouvant être appelé 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. S’il T ne s’agit pas d’une classe ou d’une interface abstraite, il T doit fournir un constructeur avec les types de (IntPtr, JniHandleOwnership) paramètres.

  4. S’il T s’agit d’une classe abstraite ou d’une interface, un appelant doit être disponible pour T . Un appelant est un type non abstrait qui hérite T ou implémente T , et a le même nom qu’un T 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 elles 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 déclarant pour le champ ou 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 de chaîne est la référence de type simplifiée 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 à toutes les autres JNIEnv méthodes qui retournent 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 par le biais d’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 d’instance : un pour la lecture des champs d’instance et l’autre pour l’écriture de champs d’instance. Tous les ensembles 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 les valeurs de champ d’instance 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 des valeurs de champ d’instance 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 des champs d’instance bool .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance sbyte .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance char .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance short .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance int .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance long .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance float .

  • JNIEnv.SetField) : écrivez la valeur des champs d’instance double .

Champs statiques

Les champs statiques sont manipulés par le biais d’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 : un pour la lecture des champs d’instance et l’autre pour l’écriture de champs d’instance. Tous les ensembles de méthodes nécessitent un ID de champ pour lire ou écrire la valeur du champ.

Lecture des valeurs de champ statique

L’ensemble de méthodes permettant de lire les valeurs de champ statiques suit le modèle d’affectation de noms :

* 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 par le biais d’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 : une pour appeler des méthodes virtuellement, et une 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 déclarant ; les méthodes provenant d’interfaces étendues/héritées ne peuvent pas être recherchées. Pour plus d’informations, consultez la section d’implémentation des interfaces de liaison /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 permettant d’appeler des méthodes suit virtuellement 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 permettant d’appeler des méthodes non virtuellement 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 par le biais d’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 permettant d’appeler des méthodes suit virtuellement 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 séparées ou autre chose), suivie d’une parenthèse '('')'fermante, suivie de la référence de type JNI du type retour de méthode.

Par exemple, étant donné la méthode Java :

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

La signature de type JNI serait la suivante :

(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 ; celles-ci 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 tels que java.lang.String 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 :

  • incorporé
  • 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 valeur intégrés. 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 void types de retour de 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ée :

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

  2. Lire 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ée.

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 des références de type Array et avec des 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 est un type d’énumération Java. Nous pouvons donc 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 de 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 existe 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 », dont les deux ont la même référence de type simplifié, "java/lang/Class".

Prise en charge de l’interface native Java

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

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 raisonnablement 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 disposez d’une instance d’objet Java conservée dans un IntPtr, vous voudrez probablement faire quelque chose avec celui-ci. Vous pouvez utiliser des méthodes JNIEnv telles que JNIEnv.CallVoidMethod() pour le faire, mais s’il existe déjà un wrapper C# analogique, vous souhaiterez construire un wrapper sur la référence JNI. Vous pouvez le faire via la méthode d’extension Extensions.JavaCast<T> :

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 Java.Lang.Object.GetObject<T> :

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 devrait être évitée à tous les coûts. Malheureusement, il n’est pas toujours évitable ; espérons que ce guide fournira une assistance lorsque vous atteignez les cas Java non liés avec Mono pour Android.