Uso di JNI e Xamarin.Android

Xamarin.Android consente di scrivere app Android con C# invece di Java. Sono disponibili diversi assembly con Xamarin.Android che forniscono associazioni per le librerie Java, tra cui Mono.Android.dll e Mono.Android.Google Mappe.dll. Tuttavia, le associazioni non vengono fornite per tutte le possibili librerie Java e le associazioni fornite potrebbero non associare tutti i tipi e i membri Java. Per usare tipi e membri Java non associati, è possibile usare Java Native Interface (JNI). Questo articolo illustra come usare JNI per interagire con tipi e membri Java dalle applicazioni Xamarin.Android.

Panoramica

Non è sempre necessario o possibile creare un Wrapper chiamabile gestito (MCW) per richiamare il codice Java. In molti casi, JNI "inline" è perfettamente accettabile e utile per l'uso occasionale di membri Java non associati. Spesso è più semplice usare JNI per richiamare un singolo metodo in una classe Java rispetto a generare un'intera associazione .jar.

Xamarin.Android fornisce l'assembly, che fornisce un'associazione Mono.Android.dll per la libreria di android.jar Android. I tipi e i membri non presenti all'interno di e i tipi non presenti all'interno Mono.Android.dllandroid.jar possono essere usati associandoli manualmente. Per associare tipi e membri Java, usare Java Native Interface (JNI) per cercare tipi, leggere e scrivere campi e richiamare i metodi.

L'API JNI in Xamarin.Android è concettualmente molto simile all'API System.Reflection in .NET: consente di cercare tipi e membri in base al nome, leggere e scrivere valori di campo, richiamare metodi e altro ancora. È possibile usare JNI e l'attributo personalizzato per dichiarare metodi virtuali che possono essere associati per supportare l'override Android.Runtime.RegisterAttribute . È possibile associare interfacce in modo che possano essere implementate in C#.

Questo documento illustra:

  • In che modo JNI fa riferimento ai tipi.
  • Come cercare, leggere e scrivere campi.
  • Come cercare e richiamare i metodi.
  • Come esporre metodi virtuali per consentire l'override dal codice gestito.
  • Come esporre le interfacce.

Requisiti

JNI, come esposto tramite lo spazio dei nomi Android.Runtime.JNIEnv, è disponibile in ogni versione di Xamarin.Android. Per associare tipi e interfacce Java, è necessario usare Xamarin.Android 4.0 o versione successiva.

Wrapper chiamabili gestiti

Un wrapper chiamabile gestito (MCW) è un'associazione per una classe o un'interfaccia Java che esegue il wrapping di tutti i macchinari JNI in modo che il codice C# client non debba preoccuparsi della complessità sottostante di JNI. La maggior parte di Mono.Android.dll è costituita da wrapper chiamabili gestiti.

I wrapper chiamabili gestiti servono due scopi:

  1. Incapsulare l'uso di JNI in modo che il codice client non debba conoscere la complessità sottostante.
  2. Rendere possibile i tipi Java di sottoclasse e implementare interfacce Java.

Il primo scopo è puramente per praticità e incapsulamento della complessità in modo che i consumer abbiano un set semplice e gestito di classi da usare. Questo richiede l'uso dei vari membri JNIEnv , come descritto più avanti in questo articolo. Tenere presente che i wrapper chiamabili gestiti non sono strettamente necessari: l'uso JNI "inline" è perfettamente accettabile ed è utile per l'uso uni-off dei membri Java non associati. L'implementazione di sottoclassi e interfacce richiede l'uso di wrapper chiamabili gestiti.

Android Callable Wrapper

I wrapper chiamabili Android (ACW) sono necessari ogni volta che il runtime Android (ART) deve richiamare il codice gestito; questi wrapper sono necessari perché non è possibile registrare le classi con ART in fase di esecuzione. (In particolare, il La funzione JNI DefineClass non è supportata dal runtime Android. I wrapper chiamabili Android costituiscono quindi la mancanza di supporto per la registrazione dei tipi di runtime.

Ogni volta che il codice Android deve eseguire un metodo virtuale o di interfaccia sottoposto a override o implementato nel codice gestito, Xamarin.Android deve fornire un proxy Java in modo che questo metodo venga inviato al tipo gestito appropriato. Questi tipi di proxy Java sono codice Java con la "stessa" classe di base e l'elenco di interfacce Java del tipo gestito, implementando gli stessi costruttori e dichiarando eventuali metodi di interfaccia e classe base sottoposti a override.

I wrapper chiamabili Android vengono generati dal programma monodroid.exe durante il processo di compilazione e vengono generati per tutti i tipi che (direttamente o indirettamente) ereditano Java.Lang.Object.

Implementazione di interfacce

In alcuni casi potrebbe essere necessario implementare un'interfaccia Android, ad esempio Android.Content.IComponentCallbacks.

Tutte le classi e le interfacce Android estendono l'interfaccia Android.Runtime.IJavaObject , pertanto tutti i tipi Android devono implementare IJavaObject. Xamarin.Android sfrutta questo fatto: usa IJavaObject per fornire ad Android un proxy Java (un wrapper chiamabile Android) per il tipo gestito specificato. Poiché monodroid.exe cerca Java.Lang.Object solo sottoclassi (che devono implementare IJavaObject), la sottoclasse Java.Lang.Object fornisce un modo per implementare le interfacce nel codice gestito. Ad esempio:

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

Dettagli dell'implementazione

La parte restante di questo articolo fornisce dettagli sull'implementazione soggetti a modifiche senza preavviso (e viene presentato qui solo perché gli sviluppatori potrebbero essere curiosi di cosa sta succedendo sotto il cappuccio).

Ad esempio, data l'origine C# seguente:

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);
        }
    }
}

Il programma mandroid.exe genererà il wrapper chiamabile Android seguente:

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);
}

Si noti che la classe base viene mantenuta e vengono fornite dichiarazioni di metodo native per ogni metodo sottoposto a override all'interno del codice gestito.

ExportAttribute ed ExportFieldAttribute

In genere, Xamarin.Android genera automaticamente il codice Java che comprende l'AW; questa generazione si basa sui nomi di classe e metodo quando una classe deriva da una classe Java ed esegue l'override dei metodi Java esistenti. In alcuni scenari, tuttavia, la generazione del codice non è adeguata, come descritto di seguito:

  • Android supporta i nomi di azione negli attributi XML di layout, ad esempio l'attributo XML android:onClick . Quando viene specificato, l'istanza di View gonfiata tenta di cercare il metodo Java.

  • L'interfaccia java.io.Serializable richiede readObject metodi e writeObject . Poiché non sono membri di questa interfaccia, l'implementazione gestita corrispondente non espone questi metodi al codice Java.

  • L'interfaccia android.os.Parcelable prevede che una classe di implementazione abbia un campo CREATOR statico di tipo Parcelable.Creator. Il codice Java generato richiede un campo esplicito. Con lo scenario standard, non esiste alcun modo per restituire il campo nel codice Java dal codice gestito.

Poiché la generazione di codice non fornisce una soluzione per generare metodi Java arbitrari con nomi arbitrari, a partire da Xamarin.Android 4.2, Sono stati introdotti ExportAttribute ed ExportFieldAttribute per offrire una soluzione agli scenari precedenti. Entrambi gli attributi si trovano nello spazio dei Java.Interop nomi :

  • ExportAttribute : specifica un nome di metodo e i relativi tipi di eccezione previsti (per fornire "throw" espliciti in Java). Quando viene usato in un metodo, il metodo "esporta" un metodo Java che genera un codice dispatch alla chiamata JNI corrispondente al metodo gestito. Può essere usato con android:onClick e java.io.Serializable.

  • ExportFieldAttribute : specifica un nome di campo. Si trova in un metodo che funziona come inizializzatore di campo. Può essere usato con android.os.Parcelable.

Risoluzione dei problemi relativi a ExportAttribute ed ExportFieldAttribute

  • La creazione di pacchetti non riesce a causa di Mono.Android.Export.dll mancanti: se sono stati usati ExportAttribute o ExportFieldAttribute in alcuni metodi nel codice o nelle librerie dipendenti, è necessario aggiungere Mono.Android.Export.dll. Questo assembly è isolato per supportare il codice di callback da Java. È separato da Mono.Android.dll in quanto aggiunge dimensioni aggiuntive all'applicazione.

  • Nella build di rilascio si MissingMethodException verifica per i metodi di esportazione: nella build release si MissingMethodException verifica per i metodi di esportazione. Questo problema è stato risolto nella versione più recente di Xamarin.Android.

ExportParameterAttribute

ExportAttribute e ExportFieldAttribute forniscono funzionalità che il codice di runtime Java può usare. Questo codice di runtime accede al codice gestito tramite i metodi JNI generati basati su tali attributi. Di conseguenza, non esiste alcun metodo Java esistente associato dal metodo gestito; di conseguenza, il metodo Java viene generato da una firma del metodo gestito.

Tuttavia, questo caso non è completamente determinante. In particolare, questo vale in alcuni mapping avanzati tra i tipi gestiti e i tipi Java, ad esempio:

  • InputStream
  • Outputstream
  • XmlPullParser
  • XmlResourceParser

Quando sono necessari tipi come questi per i metodi esportati, è necessario utilizzare per ExportParameterAttribute assegnare in modo esplicito al parametro corrispondente o al valore restituito un tipo.

Attributo annotazione

In Xamarin.Android 4.2 i tipi di implementazione sono stati convertiti IAnnotation in attributi (System.Attribute) e sono stati aggiunti il supporto per la generazione di annotazioni nei wrapper Java.

Ciò significa che le modifiche direzionali seguenti:

  • Il generatore di binding genera Java.Lang.DeprecatedAttribute da java.Lang.Deprecated (mentre deve essere [Obsolete] nel codice gestito).

  • Questo non significa che la classe esistente Java.Lang.Deprecated svanirà. Questi oggetti basati su Java possono essere comunque usati come oggetti Java normali (se tali utilizzi esistono). Ci saranno Deprecated classi e DeprecatedAttribute .

  • La Java.Lang.DeprecatedAttribute classe è contrassegnata come [Annotation] . Quando è presente un attributo personalizzato ereditato da questo [Annotation] attributo, l'attività msbuild genererà un'annotazione Java per tale attributo personalizzato (@Deprecated) in Android Callable Wrapper (ACW).

  • Le annotazioni possono essere generate in classi, metodi ed esportati campi (ovvero un metodo nel codice gestito).

Se la classe contenitore (la classe con annotazioni o la classe che contiene i membri con annotazioni) non viene registrata, l'intera origine della classe Java non viene generata affatto, incluse le annotazioni. Per i metodi, è possibile specificare per ExportAttribute ottenere il metodo generato e annotato in modo esplicito. Inoltre, non è una funzionalità per "generare" una definizione di classe di annotazione Java. In altre parole, se si definisce un attributo gestito personalizzato per una determinata annotazione, sarà necessario aggiungere un'altra libreria .jar contenente la classe di annotazione Java corrispondente. L'aggiunta di un file di origine Java che definisce il tipo di annotazione non è sufficiente. Il compilatore Java non funziona allo stesso modo di apt.

Si applicano anche le limitazioni seguenti:

  • Questo processo di conversione non considera @Target finora l'annotazione sul tipo di annotazione.

  • Gli attributi in una proprietà non funzionano. Usare invece gli attributi per il getter o il setter della proprietà.

Associazione di classi

L'associazione di una classe significa scrivere un wrapper chiamabile gestito per semplificare la chiamata del tipo Java sottostante.

L'associazione di metodi virtuali e astratti per consentire l'override da C# richiede Xamarin.Android 4.0. Tuttavia, qualsiasi versione di Xamarin.Android può associare metodi non virtuali, metodi statici o metodi virtuali senza supportare gli override.

Un'associazione contiene in genere gli elementi seguenti:

Dichiarazione dell'handle di tipo

I metodi di ricerca di campi e metodi richiedono un riferimento all'oggetto che fa riferimento al tipo dichiarante. Per convenzione, questo si trova in un class_ref campo:

static IntPtr class_ref = JNIEnv.FindClass(CLASS);

Per informazioni dettagliate sul CLASS token, vedere la sezione Riferimenti ai tipi JNI.

Campi di associazione

I campi Java vengono esposti come proprietà C#, ad esempio il campo Java java.lang.System.in è associato come proprietà C# Java.Lang.JavaSystem.In. Inoltre, poiché JNI distingue tra campi statici e campi di istanza, vengono usati metodi diversi per l'implementazione delle proprietà.

L'associazione di campi prevede tre set di metodi:

  1. Metodo get field id . Il metodo get field id è responsabile della restituzione di un handle di campo che verrà utilizzato dai metodi get field value e set field value . Per ottenere l'ID campo è necessario conoscere il tipo dichiarante, il nome del campo e la firma del tipo JNI del campo.

  2. Metodi get field value . Questi metodi richiedono l'handle di campo e sono responsabili della lettura del valore del campo da Java. Il metodo da utilizzare dipende dal tipo del campo.

  3. Metodi set field value . Questi metodi richiedono l'handle di campo e sono responsabili della scrittura del valore del campo in Java. Il metodo da utilizzare dipende dal tipo del campo.

I campi statici usano i metodi JNIEnv.GetStaticFieldID, JNIEnv.GetStatic*Fielde JNIEnv.SetStaticField .

I campi dell'istanza usano i metodi JNIEnv.GetFieldID, JNIEnv.Get*Fielde JNIEnv.SetField .

Ad esempio, la proprietà JavaSystem.In statica può essere implementata come segue:

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);
    }
}

Nota: si usa InputStreamInvoker.FromJniHandle per convertire il riferimento JNI in un'istanza System.IO.Stream e viene usato JniHandleOwnership.TransferLocalRef perché JNIEnv.GetStaticObjectField restituisce un riferimento locale.

Molti dei tipi Android.Runtime hanno FromJniHandle metodi che convertiranno un riferimento JNI nel tipo desiderato.

Associazione di metodi

I metodi Java vengono esposti come metodi C# e come proprietà C#. Ad esempio, il metodo Java.lang.Runtime.runFinalizersOnExit viene associato come metodo Java.Lang.Runtime.RunFinalizersOnExit e il metodo java.lang.Object.getClass viene associato come proprietà Java.Lang.Object.Class.

La chiamata al metodo è un processo in due passaggi:

  1. ID del metodo get da richiamare. Il metodo get method id è responsabile della restituzione di un handle di metodo che verrà utilizzato dai metodi di chiamata al metodo. Per ottenere l'ID del metodo è necessario conoscere il tipo dichiarante, il nome del metodo e la firma del tipo JNI del metodo.

  2. Richiamare il metodo.

Analogamente ai campi, i metodi da usare per ottenere l'ID del metodo e richiamare il metodo differiscono tra metodi statici e metodi di istanza.

I metodi statici usano JNIEnv.GetStaticMethodID() per cercare l'ID del metodo e usare la famiglia di metodi per la JNIEnv.CallStatic*Method chiamata.

I metodi di istanza usano JNIEnv.GetMethodID per cercare l'ID del metodo e usare le famiglie di metodi e JNIEnv.CallNonvirtual*Method per la JNIEnv.Call*Method chiamata.

L'associazione di metodi è potenzialmente più di una semplice chiamata al metodo. L'associazione di metodi include anche la possibilità di eseguire l'override di un metodo (per i metodi astratti e non finali) o implementati (per i metodi di interfaccia). La sezione Supporto dell'ereditarietà, delle interfacce illustra le complessità del supporto di metodi virtuali e metodi di interfaccia.

Metodi statici

L'associazione di un metodo statico comporta l'uso JNIEnv.GetStaticMethodID di per ottenere un handle di metodo, quindi l'uso del metodo appropriato JNIEnv.CallStatic*Method , a seconda del tipo restituito del metodo. Di seguito è riportato un esempio di associazione per il metodo 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);
}

Si noti che l'handle del metodo viene archiviato in un campo statico, id_getRuntime. Si tratta di un'ottimizzazione delle prestazioni, in modo che l'handle del metodo non debba essere cercato in ogni chiamata. Non è necessario memorizzare nella cache l'handle del metodo in questo modo. Dopo aver ottenuto l'handle del metodo, JNIEnv.CallStaticObjectMethod viene usato per richiamare il metodo . JNIEnv.CallStaticObjectMethod restituisce un oggetto IntPtr che contiene l'handle dell'istanza Java restituita. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) viene usato per convertire l'handle Java in un'istanza di oggetto fortemente tipizzata.

Associazione del metodo di istanza non virtuale

L'associazione di un final metodo di istanza o di un metodo di istanza che non richiede l'override comporta l'uso JNIEnv.GetMethodID di per ottenere un handle di metodo, quindi l'uso del metodo appropriato JNIEnv.Call*Method , a seconda del tipo restituito del metodo. Di seguito è riportato un esempio di associazione per la Object.Class proprietà :

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);
    }
}

Si noti che l'handle del metodo viene archiviato in un campo statico, id_getClass. Si tratta di un'ottimizzazione delle prestazioni, in modo che l'handle del metodo non debba essere cercato in ogni chiamata. Non è necessario memorizzare nella cache l'handle del metodo in questo modo. Dopo aver ottenuto l'handle del metodo, JNIEnv.CallStaticObjectMethod viene usato per richiamare il metodo . JNIEnv.CallStaticObjectMethod restituisce un oggetto IntPtr che contiene l'handle dell'istanza Java restituita. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) viene usato per convertire l'handle Java in un'istanza di oggetto fortemente tipizzata.

Costruttori di binding

I costruttori sono metodi Java con il nome "<init>". Analogamente ai metodi di istanza Java, JNIEnv.GetMethodID viene usato per cercare l'handle del costruttore. A differenza dei metodi Java, i metodi JNIEnv.NewObject vengono usati per richiamare l'handle del metodo del costruttore. Il valore restituito di JNIEnv.NewObject è un riferimento 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…

In genere un'associazione di classi sottoclasserà Java.Lang.Object. Quando si sottoclassa Java.Lang.Object, entra in gioco una semantica aggiuntiva: un'istanza Java.Lang.Object mantiene un riferimento globale a un'istanza Java tramite la Java.Lang.Object.Handle proprietà .

  1. Il Java.Lang.Object costruttore predefinito allocherà un'istanza Java.

  2. Se il tipo ha un RegisterAttribute e RegisterAttribute.DoNotGenerateAcw è true , viene creata un'istanza del tipo tramite il RegisterAttribute.Name relativo costruttore predefinito.

  3. In caso contrario, viene creata un'istanza di Android Callable Wrapper (ACW) corrispondente this.GetType tramite il relativo costruttore predefinito. I wrapper chiamabili Android vengono generati durante la creazione del pacchetto per ogni Java.Lang.Object sottoclasse per cui RegisterAttribute.DoNotGenerateAcw non è impostato su true.

Per i tipi che non sono associazioni di classi, si tratta della semantica prevista: la creazione di un'istanza Mono.Samples.HelloWorld.HelloAndroid C# deve creare un'istanza Java mono.samples.helloworld.HelloAndroid che è un wrapper chiamabile Android generato.

Per le associazioni di classe, questo potrebbe essere il comportamento corretto se il tipo Java contiene un costruttore predefinito e/o nessun altro costruttore deve essere richiamato. In caso contrario, è necessario specificare un costruttore che esegue le azioni seguenti:

  1. Richiamare Java.Lang.Object(IntPtr, JniHandleOwnership) anziché il costruttore predefinito Java.Lang.Object . Questa operazione è necessaria per evitare di creare una nuova istanza Java.

  2. Controllare il valore di Java.Lang.Object.Handle prima di creare istanze Java. La Object.Handle proprietà avrà un valore diverso da IntPtr.Zero se un wrapper chiamabile Android è stato costruito nel codice Java e l'associazione di classe viene costruita per contenere l'istanza di Android Callable Wrapper creata. Ad esempio, quando Android crea un'istanza mono.samples.helloworld.HelloAndroid , android Callable Wrapper verrà creato per primo e il costruttore Java HelloAndroid creerà un'istanza del tipo corrispondente Mono.Samples.HelloWorld.HelloAndroid , con la Object.Handle proprietà impostata sull'istanza Java prima dell'esecuzione del costruttore.

  3. Se il tipo di runtime corrente non corrisponde al tipo dichiarante, è necessario creare un'istanza del wrapper chiamabile Android corrispondente e usare Object.SetHandle per archiviare l'handle restituito da JNIEnv.CreateInstance.

  4. Se il tipo di runtime corrente è uguale al tipo dichiarante, richiamare il costruttore Java e usare Object.SetHandle per archiviare l'handle restituito da JNIEnv.NewInstance .

Si consideri ad esempio il costruttore java.lang.Integer(int). Questo è associato come segue:

// 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);
}

I metodi JNIEnv.CreateInstance sono helper per eseguire un JNIEnv.FindClassoggetto , JNIEnv.GetMethodID, JNIEnv.NewObjecte JNIEnv.DeleteGlobalReference sul valore restituito da JNIEnv.FindClass. Per informazioni dettagliate, vedere la sezione che segue.

Supporto dell'ereditarietà, delle interfacce

La sottoclassazione di un tipo Java o l'implementazione di un'interfaccia Java richiede la generazione di ACL (Callable Wrapper ) Android generati per ogni Java.Lang.Object sottoclasse durante il processo di creazione del pacchetto. La generazione di ACW viene controllata tramite l'attributo personalizzato Android.Runtime.RegisterAttribute .

Per i tipi C#, il [Register] costruttore dell'attributo personalizzato richiede un argomento: il riferimento al tipo semplificato JNI per il tipo Java corrispondente. In questo modo è possibile specificare nomi diversi tra Java e C#.

Prima di Xamarin.Android 4.0, l'attributo [Register] personalizzato non era disponibile per i tipi Java esistenti "alias". Ciò è dovuto al fatto che il processo di generazione acw genera gli ACL per ogni Java.Lang.Object sottoclasse rilevata.

Xamarin.Android 4.0 ha introdotto la proprietà RegisterAttribute.DoNotGenerateAcw . Questa proprietà indica al processo di generazione ACW di ignorare il tipo annotato, consentendo la dichiarazione di nuovi wrapper chiamabili gestiti che non comportano la generazione di ACL al momento della creazione del pacchetto. In questo modo è possibile eseguire l'associazione di tipi Java esistenti. Si consideri, ad esempio, la classe Java semplice seguente, Adder, che contiene un metodo, add, che aggiunge a numeri interi e restituisce il risultato:

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

Il Adder tipo può essere associato come segue:

[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 {
}

In questo caso, il Adder tipo C# aliasa il Adder tipo Java. L'attributo [Register] viene usato per specificare il nome JNI del mono.android.test.Adder tipo Java e la proprietà viene usata per inibire la DoNotGenerateAcw generazione di AW. Ciò comporterà la generazione di un AW per il ManagedAdder tipo, che sottoclassa correttamente il mono.android.test.Adder tipo. Se la RegisterAttribute.DoNotGenerateAcw proprietà non fosse stata usata, il processo di compilazione di Xamarin.Android avrebbe generato un nuovo mono.android.test.Adder tipo Java. Ciò comporterebbe errori di compilazione, perché il mono.android.test.Adder tipo sarebbe presente due volte, in due file separati.

Binding di metodi virtuali

ManagedAdder sottoclassi del tipo Java Adder , ma non è particolarmente interessante: il tipo C# Adder non definisce metodi virtuali, quindi ManagedAdder non può eseguire l'override di nulla.

I virtual metodi di associazione per consentire l'override da sottoclassi richiedono diverse operazioni che devono essere eseguite che rientrano nelle due categorie seguenti:

  1. Associazione di metodi

  2. Registrazione del metodo

Associazione di metodi

Un'associazione di metodi richiede l'aggiunta di due membri di supporto alla definizione C# Adder : ThresholdTypee ThresholdClass.

ThresholdType

La ThresholdType proprietà restituisce il tipo corrente dell'associazione:

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

ThresholdType viene usato nell'associazione di metodi per determinare quando deve eseguire l'invio di metodi virtuali e non virtuali. Deve restituire sempre un'istanza System.Type che corrisponde al tipo C# dichiarante.

ThresholdClass

La ThresholdClass proprietà restituisce il riferimento alla classe JNI per il tipo associato:

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

ThresholdClass viene utilizzato nell'associazione di metodi quando si richiamano metodi non virtuali.

Implementazione dell'associazione

L'implementazione dell'associazione di metodi è responsabile della chiamata in fase di esecuzione del metodo Java. Contiene anche una [Register] dichiarazione di attributo personalizzata che fa parte della registrazione del metodo e verrà illustrata nella sezione Registrazione metodo:

[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));
    }
}

Il id_add campo contiene l'ID del metodo per il metodo Java da richiamare. Il id_add valore viene ottenuto da JNIEnv.GetMethodID, che richiede la classe dichiarante (class_ref), il nome del metodo Java ("add") e la firma JNI del metodo ("(II)I").

Dopo aver ottenuto l'ID del metodo, GetType viene confrontato per ThresholdType determinare se è necessario inviare virtuale o non virtuale. L'invio virtuale è obbligatorio quando GetType corrisponde ThresholdTypea , come Handle può fare riferimento a una sottoclasse allocata da Java che esegue l'override del metodo .

Quando GetType non corrisponde ThresholdTypea , Adder è stato sottoclassato (ad esempio, da ManagedAdder) e l'implementazione Adder.Add verrà richiamata solo se la sottoclasse ha richiamato base.Add. Questo è il caso di invio non virtuale, che è dove ThresholdClass entra in gioco. ThresholdClass specifica la classe Java che fornirà l'implementazione del metodo da richiamare.

Registrazione del metodo

Si supponga di avere una definizione aggiornata ManagedAdder che esegue l'override del Adder.Add metodo :

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

Tenere presente che Adder.Add aveva un [Register] attributo personalizzato:

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

Il costruttore dell'attributo [Register] personalizzato accetta tre valori:

  1. Nome del metodo Java, "add" in questo caso.

  2. La firma del tipo JNI del metodo, "(II)I" in questo caso.

  3. Metodo del connettore , GetAddHandler in questo caso. Connessione or verranno illustrati più avanti.

I primi due parametri consentono al processo di generazione ACW di generare una dichiarazione di metodo per eseguire l'override del metodo. L'AW risultante conterrà alcuni dei codici seguenti:

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);
    // ...
}

Si noti che un @Override metodo viene dichiarato, che delega a un n_metodo con prefisso con lo stesso nome. In questo modo, quando il codice Java richiama ManagedAdder.add, ManagedAdder.n_add verrà richiamato, che consentirà l'esecuzione del metodo C# ManagedAdder.Add di override.

Quindi, la domanda più importante: come viene ManagedAdder.n_add collegato a ManagedAdder.Add?

I metodi Java native vengono registrati con il runtime Java (il runtime Android) tramite la funzione RegisterNatives JNI. RegisterNatives accetta una matrice di strutture contenenti il nome del metodo Java, la firma del tipo JNI e un puntatore a funzione per richiamare che segue la convenzione di chiamata JNI. Il puntatore di funzione deve essere una funzione che accetta due argomenti puntatore seguiti dai parametri del metodo. Il metodo Java ManagedAdder.n_add deve essere implementato tramite una funzione con il prototipo C seguente:

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

Xamarin.Android non espone un RegisterNatives metodo. Al contrario, acw e MCW forniscono insieme le informazioni necessarie per richiamare RegisterNatives: l'AW contiene il nome del metodo e la firma del tipo JNI, l'unica cosa mancante è un puntatore a funzione da associare.

È qui che entra in gioco il metodo del connettore. Il terzo [Register] parametro dell'attributo personalizzato è il nome di un metodo definito nel tipo registrato o una classe base del tipo registrato che non accetta parametri e restituisce un oggetto System.Delegate. L'oggetto restituito System.Delegate a sua volta fa riferimento a un metodo con la firma della funzione JNI corretta. Infine, il delegato restituito dal metodo del connettore deve essere rooted in modo che il GC non lo raccolga, perché il delegato viene fornito a 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

Il GetAddHandler metodo crea un Func<IntPtr, IntPtr, int, int, int> delegato che fa riferimento al n_Add metodo , quindi richiama JNINativeWrapper.CreateDelegate. JNINativeWrapper.CreateDelegate esegue il wrapping del metodo fornito in un blocco try/catch, in modo che tutte le eccezioni non gestite vengano gestite e generano l'evento AndroidEvent.UnhandledExceptionRaiser . Il delegato risultante viene archiviato nella variabile statica cb_add in modo che il GC non liberi il delegato.

Infine, il metodo è responsabile del n_Add marshalling dei parametri JNI ai tipi gestiti corrispondenti, quindi delegando la chiamata al metodo.

Nota: usare JniHandleOwnership.DoNotTransfer sempre quando si ottiene un MCW su un'istanza Java. Considerarli come un riferimento locale (e quindi chiamare JNIEnv.DeleteLocalRef) interromperà le transizioni dello stack gestito -> Java -> stack gestito.

Associazione completa adder

L'associazione gestita completa per il mono.android.tests.Adder tipo è:

[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
}

Restrizioni

Quando si scrive un tipo che corrisponde ai criteri seguenti:

  1. Sottoclassi Java.Lang.Object

  2. Ha un [Register] attributo personalizzato

  3. RegisterAttribute.DoNotGenerateAcw è true

Quindi per l'interazione GC il tipo non deve avere campi che possono fare riferimento a una Java.Lang.Object sottoclasse o Java.Lang.Object in fase di esecuzione. Ad esempio, i campi di tipo System.Object e qualsiasi tipo di interfaccia non sono consentiti. I tipi che non possono fare riferimento alle Java.Lang.Object istanze sono consentiti, ad esempio System.String e List<int>. Questa restrizione consiste nel impedire la raccolta di oggetti prematuri da parte del GC.

Se il tipo deve contenere un campo di istanza che può fare riferimento a un'istanza Java.Lang.Object di , il tipo di campo deve essere System.WeakReference o GCHandle.

Binding di metodi astratti

I metodi di associazione abstract sono in gran parte identici ai metodi virtuali di associazione. Esistono solo due differenze:

  1. Il metodo astratto è astratto. Mantiene ancora l'attributo [Register] e la registrazione del metodo associata, l'associazione al metodo viene spostata nel Invoker tipo .

  2. Viene creato un tipo non abstractInvoker che sottoclassi il tipo astratto. Il Invoker tipo deve eseguire l'override di tutti i metodi astratti dichiarati nella classe di base e l'implementazione sottoposta a override è l'implementazione dell'associazione di metodi, anche se il caso dispatch non virtuale può essere ignorato.

Si supponga, ad esempio, che il metodo precedente mono.android.test.Adder.add fosse abstract. L'associazione C# verrà modificata in modo che Adder.Add fosse astratta e verrà definito un nuovo AdderInvoker tipo che implementa 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));
    }
}

Il Invoker tipo è necessario solo quando si ottengono riferimenti JNI alle istanze create da Java.

Interfacce di binding

Le interfacce di associazione sono concettualmente simili alle classi di associazione contenenti metodi virtuali, ma molte delle specifiche differiscono in modi sottili (e non così sottili). Si consideri la dichiarazione di interfaccia Java seguente:

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

Le associazioni di interfaccia hanno due parti: la definizione dell'interfaccia C# e una definizione invoker per l'interfaccia.

Definizione dell'interfaccia

La definizione dell'interfaccia C# deve soddisfare i requisiti seguenti:

  • La definizione dell'interfaccia deve avere un [Register] attributo personalizzato.

  • La definizione dell'interfaccia deve estendere .IJavaObject interface In caso contrario, gli ACL non ereditano dall'interfaccia Java.

  • Ogni metodo di interfaccia deve contenere un [Register] attributo che specifica il nome del metodo Java corrispondente, la firma JNI e il metodo del connettore.

  • Il metodo connettore deve inoltre specificare il tipo in cui è possibile individuare il metodo del connettore.

Quando si esegue l'associazione abstract e virtual i metodi, il metodo connettore viene cercato all'interno della gerarchia di ereditarietà del tipo registrato. Le interfacce non possono avere metodi contenenti corpi, quindi questo non funziona, pertanto il requisito che un tipo venga specificato che indica dove si trova il metodo del connettore. Il tipo viene specificato all'interno della stringa del metodo del connettore, dopo i due punti ':'e deve essere il nome del tipo completo dell'assembly del tipo contenente il invoker.

Le dichiarazioni dei metodi di interfaccia sono una traduzione del metodo Java corrispondente usando tipi compatibili . Per i tipi predefiniti Java, i tipi compatibili sono i tipi C# corrispondenti, ad esempio Java int è C# int. Per i tipi di riferimento, il tipo compatibile è un tipo che può fornire un handle JNI del tipo Java appropriato.

I membri dell'interfaccia non verranno richiamati direttamente da Java. La chiamata verrà mediata tramite il tipo invoker, quindi è consentita una certa flessibilità.

L'interfaccia Java Progress può essere dichiarata in C# come:

[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);
}

Si noti che il parametro Java int[] viene mappato a un codice JavaArray<int>. Questo non è necessario: potrebbe essere stato associato a un oggetto C# int[]o a un IList<int>oggetto o a un'altra operazione completamente diversa. Qualsiasi tipo venga scelto, deve Invoker essere in grado di convertirlo in un tipo Java int[] per la chiamata.

Definizione invoker

La Invoker definizione del tipo deve ereditare , implementare Java.Lang.Objectl'interfaccia appropriata e fornire tutti i metodi di connessione a cui viene fatto riferimento nella definizione dell'interfaccia. Esiste un altro suggerimento che differisce da un'associazione di classi: gli class_ref ID campo e metodo devono essere membri dell'istanza, non membri statici.

Il motivo per cui i membri dell'istanza preferita hanno a che fare con JNIEnv.GetMethodID il comportamento nel runtime Android. Può trattarsi anche di un comportamento Java che non è stato testato. JNIEnv.GetMethodID restituisce null durante la ricerca di un metodo proveniente da un'interfaccia implementata e non dall'interfaccia dichiarata. Si consideri l'interfaccia java.util.SortedMap<K, V> Java, che implementa l'interfaccia java.util.Map<K, V>. Map fornisce un metodo chiaro , pertanto una definizione apparentemente ragionevole Invoker per SortedMap sarà:

// 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'errore precedente avrà esito negativo perché JNIEnv.GetMethodID verrà restituito null durante la ricerca del Map.clear metodo tramite l'istanza della SortedMap classe .

Esistono due soluzioni a questo scopo: tenere traccia dell'interfaccia da cui proviene ogni metodo e avere un class_ref per ogni interfaccia oppure mantenere tutti gli elementi come membri dell'istanza ed eseguire la ricerca del metodo sul tipo di classe più derivato, non sul tipo di interfaccia. Quest'ultima viene eseguita in Mono.Android.dll.

La definizione invoker include sei sezioni: il costruttore, il Dispose metodo, i ThresholdType membri e ThresholdClass , il metodo, l'implementazione del GetObject metodo di interfaccia e l'implementazione del metodo connettore.

Costruttore

Il costruttore deve cercare la classe di runtime dell'istanza richiamata e archiviare la classe di runtime nel campo dell'istanza 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);
    }
}

Nota: la Handle proprietà deve essere usata all'interno del corpo del costruttore e non il handle parametro , come in Android v4.0 il handle parametro potrebbe non essere valido al termine dell'esecuzione del costruttore di base.

Metodo Dispose

Il Dispose metodo deve liberare il riferimento globale allocato nel costruttore:

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 e ThresholdClass

I ThresholdType membri e ThresholdClass sono identici a quanto trovato in un'associazione di classi:

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

Metodo GetObject

Per supportare Extensions.JavaCast<T>(), è necessario un metodo statico GetObject:

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

Metodi di interfaccia

Ogni metodo dell'interfaccia deve avere un'implementazione, che richiama il metodo Java corrispondente tramite 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));
    }
}

Metodi di Connessione or

I metodi del connettore e l'infrastruttura di supporto sono responsabili del marshalling dei parametri JNI ai tipi C# appropriati. Il parametro Java int[] verrà passato come JNI jintArray, che è un oggetto all'interno di IntPtr C#. Deve IntPtr essere sottoposto a marshalling a per JavaArray<int> supportare la chiamata dell'interfaccia 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);
        }
    }
}

Se int[] si preferisce rispetto JavaList<int>a , è possibile usare invece JNIEnv.GetArray():

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

Si noti, tuttavia, che JNIEnv.GetArray copia l'intera matrice tra le macchine virtuali, quindi per le matrici di grandi dimensioni questo potrebbe comportare un numero elevato di pressione GC aggiunta.

Definizione completa dell'invoker

Definizione IAdderProgressInvoker completa:

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
}

Riferimenti agli oggetti JNI

Molti metodi JNIEnv restituiscono riferimenti a oggetti JNI, simili a GCHandles. JNI offre tre diversi tipi di riferimenti a oggetti: riferimenti locali, riferimenti globali e riferimenti globali deboli. Tutti e tre sono rappresentati come System.IntPtr, ma (in base alla sezione Tipi di funzione JNI) non tutti i IntPtrtipi restituiti dai JNIEnv metodi sono riferimenti. Ad esempio, JNIEnv.GetMethodID restituisce un IntPtroggetto , ma non restituisce un riferimento a un oggetto, restituisce un oggetto jmethodID. Per informazioni dettagliate, vedere la documentazione della funzione JNI.

I riferimenti locali vengono creati dalla maggior parte dei metodi di creazione dei riferimenti. Android consente solo a un numero limitato di riferimenti locali di esistere in qualsiasi momento, in genere 512. I riferimenti locali possono essere eliminati tramite JNIEnv.DeleteLocalRef. A differenza di JNI, non tutti i metodi JNIEnv di riferimento che restituiscono riferimenti a oggetti restituiscono riferimenti locali; JNIEnv.FindClass restituisce un riferimento globale . È consigliabile eliminare i riferimenti locali nel modo più rapido possibile creando un oggetto Java.Lang.Object intorno all'oggetto e specificando JniHandleOwnership.TransferLocalRef il costruttore Java.Lang.Object(Handle IntPtr, JniHandleOwnership transfer).

I riferimenti globali vengono creati da JNIEnv.NewGlobalRef e JNIEnv.FindClass. Possono essere distrutti con JNIEnv.DeleteGlobalRef. Gli emulatori hanno un limite di 2.000 riferimenti globali in sospeso, mentre i dispositivi hardware hanno un limite di circa 52.000 riferimenti globali.

I riferimenti globali deboli sono disponibili solo in Android v2.2 (Froyo) e versioni successive. I riferimenti globali deboli possono essere eliminati con JNIEnv.DeleteWeakGlobalRef.

Gestione dei riferimenti locali JNI

I metodi JNIEnv.GetObjectField, JNIEnv.GetStaticObjectField, JNIEnv.CallObjectMethod, JNIEnv.CallNonvirtualObjectMethod e JNIEnv.CallStaticObjectMethod restituiscono un IntPtr riferimento locale JNI a un oggetto Java o IntPtr.Zero se Java ha restituito null. A causa del numero limitato di riferimenti locali che possono essere in sospeso contemporaneamente (512 voci), è consigliabile assicurarsi che i riferimenti vengano eliminati in modo tempestivo. Esistono tre modi in cui è possibile gestire i riferimenti locali: eliminarli in modo esplicito, creare un'istanza Java.Lang.Object per tenerli in attesa e usare Java.Lang.Object.GetObject<T>() per creare un wrapper chiamabile gestito intorno a essi.

Eliminazione esplicita dei riferimenti locali

JNIEnv.DeleteLocalRef viene usato per eliminare i riferimenti locali. Dopo aver eliminato il riferimento locale, non è più possibile usarlo, quindi è necessario prestare attenzione a assicurarsi che JNIEnv.DeleteLocalRef sia l'ultima operazione eseguita con il riferimento locale.

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

Wrapping con Java.Lang.Object

Java.Lang.Object fornisce un costruttore Java.Lang.Object(Handle IntPtr, trasferimento JniHandleOwnership) che può essere usato per eseguire il wrapping di un riferimento JNI di uscita. Il parametro JniHandleOwnership determina la modalità di trattamento del IntPtr parametro:

  • JniHandleOwnership.DoNotTransfer : l'istanza creata Java.Lang.Object creerà un nuovo riferimento globale dal handle parametro e handle rimane invariato. Il chiamante è responsabile della liberazione handle di , se necessario.

  • JniHandleOwnership.TransferLocalRef : l'istanza creata Java.Lang.Object creerà un nuovo riferimento globale dal handle parametro e handle viene eliminato con JNIEnv.DeleteLocalRef . Il chiamante non deve liberare handle e non deve usare handle al termine dell'esecuzione del costruttore.

  • JniHandleOwnership.TransferGlobalRef : l'istanza creata Java.Lang.Object assumerà la proprietà del handle parametro. Il chiamante non deve liberare handle .

Poiché i metodi di chiamata al metodo JNI restituiscono riferimenti locali, JniHandleOwnership.TransferLocalRef in genere vengono usati:

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

Il riferimento globale creato non verrà liberato finché l'istanza Java.Lang.Object non viene sottoposto a Garbage Collection. Se è possibile, l'eliminazione dell'istanza consente di liberare il riferimento globale, velocizzando le operazioni di Garbage Collection:

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

Uso di Java.Lang.Object.GetObject<T>()

Java.Lang.Object fornisce un metodo Java.Lang.Object.GetObject<T>(IntPtr handle, JniHandleOwnership transfer) che può essere usato per creare un wrapper chiamabile gestito del tipo specificato.

Il tipo T deve soddisfare i requisiti seguenti:

  1. T deve essere un tipo riferimento.

  2. T deve implementare l'interfaccia IJavaObject .

  3. Se T non è una classe o un'interfaccia astratta, T deve fornire un costruttore con i tipi di (IntPtr, JniHandleOwnership) parametro .

  4. Se T è una classe astratta o un'interfaccia, è necessario che sia disponibile un invoker per T . Un invoker è un tipo non astratto che eredita T o implementa T e ha lo stesso nome di T con un suffisso Invoker. Ad esempio, se T è l'interfaccia Java.Lang.IRunnable , il tipo Java.Lang.IRunnableInvoker deve esistere e deve contenere il costruttore richiesto (IntPtr, JniHandleOwnership) .

Poiché i metodi di chiamata al metodo JNI restituiscono riferimenti locali, JniHandleOwnership.TransferLocalRef in genere vengono usati:

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

Ricerca di tipi Java

Per cercare un campo o un metodo in JNI, è necessario cercare prima il tipo dichiarante per il campo o il metodo. Il metodo Android.Runtime.JNIEnv.FindClass(string)) viene usato per cercare i tipi Java. Il parametro string è il riferimento al tipo semplificato o il riferimento completo al tipo per il tipo Java. Per informazioni dettagliate sui riferimenti ai tipi semplificati e completi, vedere la sezione Riferimenti ai tipi JNI.

Nota: a differenza di ogni altro JNIEnv metodo che restituisce istanze dell'oggetto, FindClass restituisce un riferimento globale, non un riferimento locale.

Campi istanza

I campi vengono modificati tramite ID campo. Gli ID campo vengono ottenuti tramite JNIEnv.GetFieldID, che richiede la classe in cui è definito il campo, il nome del campo e la firma del tipo JNI del campo.

Gli ID campo non devono essere liberati e sono validi purché venga caricato il tipo Java corrispondente. Android attualmente non supporta lo scaricamento delle classi.

Esistono due set di metodi per la modifica dei campi dell'istanza: uno per la lettura dei campi dell'istanza e uno per la scrittura dei campi dell'istanza. Tutti i set di metodi richiedono un ID campo per leggere o scrivere il valore del campo.

Lettura dei valori dei campi dell'istanza

Il set di metodi per la lettura dei valori dei campi dell'istanza segue il modello di denominazione:

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

dove * è il tipo del campo:

Scrittura di valori dei campi dell'istanza

Il set di metodi per la scrittura dei valori dei campi dell'istanza segue il modello di denominazione:

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

dove Type è il tipo del campo:

  • JNIEnv.SetField): scrivere il valore di qualsiasi campo che non sia un tipo predefinito, ad esempio java.lang.Object , matrici e tipi di interfaccia. Il IntPtr valore può essere un riferimento locale JNI, un riferimento globale JNI, un riferimento globale debole JNI o IntPtr.Zero (per null ).

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza bool .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza sbyte .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza char .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza short .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza int .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza long .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza float .

  • JNIEnv.SetField): scrivere il valore dei campi dell'istanza double .

Campi statici

I campi statici vengono modificati tramite ID campo. Gli ID campo vengono ottenuti tramite JNIEnv.GetStaticFieldID, che richiede la classe in cui è definito il campo, il nome del campo e la firma del tipo JNI del campo.

Gli ID campo non devono essere liberati e sono validi purché venga caricato il tipo Java corrispondente. Android attualmente non supporta lo scaricamento delle classi.

Sono disponibili due set di metodi per la modifica di campi statici: uno per la lettura dei campi dell'istanza e uno per la scrittura dei campi dell'istanza. Tutti i set di metodi richiedono un ID campo per leggere o scrivere il valore del campo.

Lettura dei valori dei campi statici

Il set di metodi per la lettura dei valori dei campi statici segue il modello di denominazione:

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

dove * è il tipo del campo:

Scrittura di valori di campo statici

Il set di metodi per la scrittura di valori di campo statici segue il modello di denominazione:

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

dove Type è il tipo del campo:

Metodi di istanza

I metodi di istanza vengono richiamati tramite ID metodo. Gli ID metodo vengono ottenuti tramite JNIEnv.GetMethodID, che richiede il tipo in cui è definito il metodo, il nome del metodo e la firma del tipo JNI del metodo.

Gli ID metodo non devono essere liberati e sono validi purché venga caricato il tipo Java corrispondente. Android attualmente non supporta lo scaricamento delle classi.

Esistono due set di metodi per richiamare metodi: uno per richiamare i metodi virtualmente e uno per richiamare metodi non virtualmente. Entrambi i set di metodi richiedono un ID metodo per richiamare il metodo e la chiamata non virtuale richiede anche di specificare quale implementazione della classe deve essere richiamata.

I metodi di interfaccia possono essere cercati solo all'interno del tipo dichiarante; non è possibile cercare metodi provenienti da interfacce estese/ereditate. Per altri dettagli, vedere la sezione interfacce di binding/implementazione del invoker più avanti.

È possibile cercare qualsiasi metodo dichiarato nella classe o in qualsiasi classe di base o interfaccia implementata.

Chiamata al metodo virtuale

Il set di metodi per richiamare i metodi segue praticamente il modello di denominazione:

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

dove * è il tipo restituito del metodo.

Chiamata al metodo non virtuale

Il set di metodi per richiamare metodi non virtualmente segue il modello di denominazione:

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

dove * è il tipo restituito del metodo. La chiamata al metodo non virtuale viene in genere usata per richiamare il metodo di base di un metodo virtuale.

Metodi statici

I metodi statici vengono richiamati tramite ID metodo. Gli ID metodo vengono ottenuti tramite JNIEnv.GetStaticMethodID, che richiede il tipo in cui è definito il metodo, il nome del metodo e la firma del tipo JNI del metodo.

Gli ID metodo non devono essere liberati e sono validi purché venga caricato il tipo Java corrispondente. Android attualmente non supporta lo scaricamento delle classi.

Chiamata al metodo statico

Il set di metodi per richiamare i metodi segue praticamente il modello di denominazione:

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

dove * è il tipo restituito del metodo.

Firme di tipo JNI

Le firme dei tipi JNI sono riferimenti ai tipi JNI (anche se non sono stati semplificati i riferimenti ai tipi), ad eccezione dei metodi. Con i metodi, la firma del tipo JNI è una parentesi '('aperta , seguita dai riferimenti di tipo per tutti i tipi di parametro concatenati insieme (senza separare virgole o altro), seguita da una parentesi ')'chiusa , seguita dal riferimento al tipo JNI del tipo restituito del metodo.

Ad esempio, dato il metodo Java:

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

La firma del tipo JNI sarà:

(ILjava/lang/String;[I)J

In generale, è consigliabile usare il javap comando per determinare le firme JNI. Ad esempio, la firma del tipo JNI del metodo java.lang.Thread.State.valueOf(String) è "(Ljava/lang/String;) Ljava/lang/Thread$State;", mentre la firma del tipo JNI del metodo java.lang.Thread.State.values è "()[Ljava/lang/Thread$State;". Attenzione ai punti e virgola finali; che fanno parte della firma del tipo JNI.

Riferimenti ai tipi JNI

I riferimenti ai tipi JNI sono diversi dai riferimenti ai tipi Java. Non è possibile usare nomi di tipi Java completi, ad java.lang.String esempio con JNI, è invece necessario usare le varianti "java/lang/String" JNI o "Ljava/lang/String;", a seconda del contesto. Per informazioni dettagliate, vedere di seguito. Esistono quattro tipi di riferimenti ai tipi JNI:

  • Incorporato
  • Semplificato
  • type
  • array

Riferimenti ai tipi predefiniti

I riferimenti di tipo predefiniti sono un singolo carattere, usato per fare riferimento a tipi valore predefiniti. Il mapping è il seguente:

  • "B" per sbyte .
  • "S" per short .
  • "I" per int .
  • "J" per long .
  • "F" per float .
  • "D" per double .
  • "C" per char .
  • "Z" per bool .
  • "V" per void i tipi restituiti dal metodo.

Riferimenti ai tipi semplificati

I riferimenti ai tipi semplificati possono essere usati solo in JNIEnv.FindClass(string)). Esistono due modi per derivare un riferimento al tipo semplificato:

  1. Da un nome Java completo sostituire ogni '.' elemento all'interno del nome del pacchetto e prima del nome del tipo con '/' e ogni '.' all'interno di un nome di tipo con '$' .

  2. Leggere l'output di 'unzip -l android.jar | grep JavaName' .

Una delle due causerà il mapping del tipo Java java.lang.Thread.State al riferimento java/lang/Thread$Statedi tipo semplificato .

Riferimenti ai tipi

Un riferimento al tipo è un riferimento di tipo predefinito o un riferimento di tipo semplificato con un 'L' prefisso e un ';' suffisso. Per il tipo Java java.lang.String, il riferimento al tipo semplificato è "java/lang/String", mentre il riferimento al tipo è "Ljava/lang/String;".

I riferimenti ai tipi vengono usati con i riferimenti ai tipi di matrice e con le firme JNI.

Un modo aggiuntivo per ottenere un riferimento al tipo consiste nel leggere l'output di 'javap -s -classpath android.jar fully.qualified.Java.Name'. A seconda del tipo interessato, è possibile usare una dichiarazione del costruttore o un tipo restituito del metodo per determinare il nome JNI. Ad esempio:

$ 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 è un tipo di enumerazione Java, quindi è possibile usare la firma del valueOf metodo per determinare che il riferimento al tipo è Ljava/lang/lang/Thread$State;.

Riferimenti ai tipi di matrice

I riferimenti ai tipi di matrice sono '[' preceduti da un riferimento al tipo JNI. Non è possibile usare riferimenti ai tipi semplificati quando si specificano matrici.

Ad esempio, int[] è "[I", int[][] è "[[I"e java.lang.Object[] è "[Ljava/lang/Object;".

Generics Java e Cancellazione dei tipi

Nella maggior parte dei casi, come illustrato tramite JNI, i generics Java non esistono. Ci sono alcune "rughe", ma queste rughe sono in come Java interagisce con generics, non con come JNI cerca e richiama membri generici.

Non esiste alcuna differenza tra un tipo o un membro generico e un tipo o un membro non generico durante l'interazione tramite JNI. Ad esempio, il tipo generico java.lang.Class<T> è anche il tipo java.lang.Classgenerico "raw", entrambi con lo stesso riferimento di tipo semplificato, "java/lang/Class".

Supporto dell'interfaccia nativa Java

Android.Runtime.JNIEnv è un wrapper gestito per l'interfaccia JNI (Jave Native Interface). Le funzioni JNI vengono dichiarate all'interno della specifica dell'interfaccia nativa Java, anche se i metodi sono stati modificati per rimuovere il parametro esplicito JNIEnv* e IntPtr vengono usati invece di jobject, jclass, jmethodIDe così via. Si consideri ad esempio la funzione NewObject JNI:

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

Viene esposto come metodo JNIEnv.NewObject :

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

La traduzione tra le due chiamate è ragionevolmente semplice. In C avresti:

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'equivalente C# sarà:

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;
}

Una volta che un'istanza di Oggetto Java è contenuta in un intPtr, è probabile che si voglia eseguire un'operazione con essa. A tale scopo, è possibile usare metodi JNIEnv, ad esempio JNIEnv.CallVoidMethod(), ma se è già presente un wrapper C# analogico, è consigliabile creare un wrapper sul riferimento JNI. A tale scopo, è possibile usare il metodo di estensione 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>();

È anche possibile usare il metodo 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);

Inoltre, tutte le funzioni JNI sono state modificate rimuovendo il JNIEnv* parametro presente in ogni funzione JNI.

Riepilogo

La gestione diretta con JNI è un'esperienza terribile che dovrebbe essere evitata a tutti i costi. Purtroppo, non è sempre evitabile; spero che questa guida fornirà assistenza quando si raggiungeranno i casi Java non associati con Mono per Android.