Partilhar via


Trabalhando com JNI e Xamarin.Android

O Xamarin.Android permite escrever aplicativos Android com C# em vez de Java. Vários assemblies são fornecidos com o Xamarin.Android que fornecem associações para bibliotecas Java, incluindo Mono.Android.dll e Mono.Android.GoogleMaps.dll. No entanto, as ligações não são fornecidas para todas as bibliotecas Java possíveis, e as ligações fornecidas podem não ligar todos os tipos e membros Java. Para usar tipos e membros Java não ligados, a Java Native Interface (JNI) pode ser usada. Este artigo ilustra como usar o JNI para interagir com tipos e membros Java de aplicativos Xamarin.Android.

Visão geral

Nem sempre é necessário ou possível criar um Managed Callable Wrapper (MCW) para chamar o código Java. Em muitos casos, o JNI "embutido" é perfeitamente aceitável e útil para uso único de membros Java não vinculados. Geralmente, é mais simples usar o JNI para chamar um único método em uma classe Java do que gerar uma ligação .jar inteira.

O Xamarin.Android fornece o assembly, que fornece uma associação para a Mono.Android.dll biblioteca do android.jar Android. Tipos e membros não presentes e Mono.Android.dll tipos não presentes podem android.jar ser usados associando-os manualmente. Para ligar tipos e membros Java, use a Java Native Interface (JNI) para pesquisar tipos, ler e gravar campos e chamar métodos.

A API JNI no Xamarin.Android é conceitualmente muito semelhante à System.Reflection API no .NET: ela possibilita que você pesquise tipos e membros por nome, leia e grave valores de campo, invoque métodos e muito mais. Você pode usar o JNI e o Android.Runtime.RegisterAttribute atributo personalizado para declarar métodos virtuais que podem ser associados para dar suporte à substituição. Você pode associar interfaces para que elas possam ser implementadas em C#.

Este documento explica:

  • Como JNI se refere a tipos.
  • Como pesquisar, ler e gravar campos.
  • Como pesquisar e invocar métodos.
  • Como expor métodos virtuais para permitir a substituição do código gerenciado.
  • Como expor interfaces.

Requisitos

A JNI, conforme exposto por meio do namespace Android.Runtime.JNIEnv, está disponível em todas as versões do Xamarin.Android. Para associar tipos e interfaces Java, você deve usar o Xamarin.Android 4.0 ou posterior.

Wrappers que podem ser chamados gerenciados

Um MCW (Managed Callable Wrapper) é uma associação para uma classe ou interface Java que encapsula todo o maquinário JNI para que o código C# do cliente não precise se preocupar com a complexidade subjacente do JNI. A maioria consiste em wrappers que podem ser chamados gerenciados Mono.Android.dll .

Os wrappers que podem ser chamados gerenciados servem a duas finalidades:

  1. Encapsule o uso de JNI para que o código do cliente não precise saber sobre a complexidade subjacente.
  2. Torne possível subclasse de tipos Java e implementação de interfaces Java.

O primeiro objetivo é puramente para conveniência e encapsulamento de complexidade para que os consumidores tenham um conjunto simples e gerenciado de classes para usar. Isso requer o uso dos vários membros do JNIEnv , conforme descrito posteriormente neste artigo. Lembre-se de que os wrappers callable gerenciados não são estritamente necessários – o uso de JNI "embutido" é perfeitamente aceitável e é útil para o uso único de membros Java não vinculados. A implementação de subclasses e interfaces requer o uso de wrappers que podem ser chamados gerenciados.

Callable Wrappers do Android

Os wrappers callable do Android (ACW) são necessários sempre que o Android Runtime (ART) precisar invocar o código gerenciado; esses wrappers são necessários porque não há como registrar classes com ART em tempo de execução. (Especificamente, o A função JNI DefineClass não é compatível com o runtime do Android. Os wrappers que podem ser chamados do Android compensam a falta de suporte ao registro de tipo de tempo de execução.)

Sempre que o código Android precisar executar um método virtual ou de interface que é substituído ou implementado no código gerenciado, o Xamarin.Android deve fornecer um proxy Java para que esse método seja expedido para o tipo gerenciado apropriado. Esses tipos de proxy Java são códigos Java que têm a "mesma" classe base e lista de interface Java que o tipo gerenciado, implementando os mesmos construtores e declarando qualquer classe base e métodos de interface substituídos.

Os wrappers que podem ser chamados do Android são gerados pelo programa monodroid.exe durante o processo de compilação e são gerados para todos os tipos que (direta ou indiretamente) herdam Java.Lang.Object.

Implementando interfaces

Há momentos em que pode ser necessário implementar uma interface do Android (como Android.Content.IComponentCallbacks).

Todas as classes e interfaces do Android estendem a interface Android.Runtime.IJavaObject ; portanto, todos os tipos de Android devem implementar IJavaObjecto . O Xamarin.Android aproveita esse fato – ele usa IJavaObject para fornecer ao Android um proxy Java (um wrapper que pode ser chamado do Android) para o tipo gerenciado fornecido. Como monodroid.exe procura Java.Lang.Object apenas subclasses (que devem implementar IJavaObject), a Java.Lang.Object subclasse nos fornece uma maneira de implementar interfaces em código gerenciado. Por exemplo:

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

Detalhes da implementação

O restante deste artigo fornece detalhes de implementação sujeitos a alterações sem aviso prévio (e é apresentado aqui apenas porque os desenvolvedores podem estar curiosos sobre o que está acontecendo nos bastidores).

Por exemplo, dada a seguinte fonte C#:

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

O programa mandroid.exe gerará o seguinte Android Callable Wrapper:

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

Observe que a classe base é preservada e as declarações de método nativo são fornecidas para cada método substituído no código gerenciado.

ExportAttribute e ExportFieldAttribute

Normalmente, o Xamarin.Android gera automaticamente o código Java que compreende o ACW; essa geração é baseada nos nomes de classe e método quando uma classe deriva de uma classe Java e substitui os métodos Java existentes. No entanto, em alguns cenários, a geração de código não é adequada, conforme descrito abaixo:

  • O Android é compatível com nomes de ação em atributos XML de layout, por exemplo, o atributo XML android:onClick . Quando ele é especificado, a instância de View inflada tenta consultar o método Java.

  • A interface java.io.Serializable requer readObject métodos e writeObject . Como eles não são membros dessa interface, nossa implementação gerenciada correspondente não expõe esses métodos ao código Java.

  • A interface android.os.Parcelable espera que uma classe de implementação tenha um campo CREATOR estático do tipo Parcelable.Creator. O código Java gerado requer algum campo explícito. Com nosso cenário padrão, não há como gerar campo no código Java a partir do código gerenciado.

Como a geração de código não fornece uma solução para gerar métodos Java arbitrários com nomes arbitrários, começando com o Xamarin.Android 4.2, o ExportAttribute e o ExportFieldAttribute foram introduzidos para oferecer uma solução para os cenários acima. Ambos os Java.Interop atributos residem no namespace:

  • ExportAttribute – especifica um nome de método e seus tipos de exceção esperados (para dar "lançamentos" explícitos em Java). Quando ele é usado em um método, o método "exporta" um método Java que gera um código de despacho para a chamada JNI correspondente para o método gerenciado. Isso pode ser usado com android:onClick e java.io.Serializable.

  • ExportFieldAttribute – especifica um nome de campo. Ele reside em um método que funciona como um inicializador de campo. Isso pode ser usado com android.os.Parcelable.

Solução de problemas de ExportAttribute e ExportFieldAttribute

  • O empacotamento falha devido à falta de Mono.Android.Export.dll – se você usou ExportAttribute ou ExportFieldAttribute em alguns métodos em seu código ou bibliotecas dependentes, será necessário adicionar Mono.Android.Export.dll. Esse assembly é isolado para dar suporte ao código de retorno de chamada do Java. Ele é separado do Mono.Android.dll pois adiciona tamanho adicional ao aplicativo.

  • Na compilação de versão, MissingMethodException ocorre para métodos de exportação – na compilação de versão, MissingMethodException ocorre para métodos de exportação. (Esse problema foi corrigido na versão mais recente do Xamarin.Android.)

ExportParameterAttribute

ExportAttribute e ExportFieldAttribute fornecer funcionalidade que o código de tempo de execução Java pode usar. Esse código de tempo de execução acessa o código gerenciado por meio dos métodos JNI gerados orientados por esses atributos. Como resultado, não há nenhum método Java existente que o método gerenciado associe; portanto, o método Java é gerado a partir de uma assinatura de método gerenciado.

No entanto, este caso não é totalmente determinante. Mais notavelmente, isso é verdadeiro em alguns mapeamentos avançados entre tipos gerenciados e tipos Java, como:

  • InputStream
  • Fluxo de saída
  • XmlPullParser
  • XmlResourceParser

Quando tipos como esses são necessários para métodos exportados, o ExportParameterAttribute deve ser usado para fornecer explicitamente um tipo ao parâmetro correspondente ou ao valor retornado.

Atributo de anotação

No Xamarin.Android 4.2, convertemos IAnnotation tipos de implementação em atributos (System.Attribute) e adicionamos suporte para geração de anotações em wrappers Java.

Isso significa as seguintes mudanças direcionais:

  • O gerador de associação é gerado Java.Lang.DeprecatedAttribute a partir de java.Lang.Deprecated (embora deva estar [Obsolete] em código gerenciado).

  • Isso não significa que a classe existente Java.Lang.Deprecated desaparecerá. Esses objetos baseados em Java ainda podem ser usados como objetos Java usuais (se tal uso existir). Haverá Deprecated e DeprecatedAttribute aulas.

  • A Java.Lang.DeprecatedAttribute classe é marcada como [Annotation] . Quando houver um atributo personalizado herdado desse [Annotation] atributo, a tarefa msbuild gerará uma anotação Java para esse atributo personalizado (@Deprecated) no Android Callable Wrapper (ACW).

  • As anotações podem ser geradas em classes, métodos e campos exportados (que é um método em código gerenciado).

Se a classe que a contém (a própria classe anotada ou a classe que contém os membros anotados) não for registrada, toda a origem da classe Java não será gerada, incluindo anotações. Para métodos, você pode especificar o ExportAttribute para obter o método explicitamente gerado e anotado. Além disso, não é um recurso para "gerar" uma definição de classe de anotação Java. Em outras palavras, se você definir um atributo gerenciado personalizado para uma determinada anotação, precisará adicionar outra biblioteca .jar que contenha a classe de anotação Java correspondente. Adicionar um arquivo de código-fonte Java que defina o tipo de anotação não é suficiente. O compilador Java não funciona da mesma maneira que o apt.

Além disso, as seguintes limitações se aplicam:

  • Esse processo de conversão não considera @Target a anotação no tipo de anotação até o momento.

  • Os atributos em uma propriedade não funcionam. Em vez disso, use atributos para getter ou setter de propriedade.

Associação de classe

Vincular uma classe significa escrever um wrapper callable gerenciado para simplificar a chamada do tipo Java subjacente.

A associação de métodos virtuais e abstratos para permitir a substituição do C# requer o Xamarin.Android 4.0. No entanto, qualquer versão do Xamarin.Android pode associar métodos não virtuais, métodos estáticos ou métodos virtuais sem dar suporte a substituições.

Uma associação normalmente contém os seguintes itens:

Declarando o identificador de tipo

Os métodos de pesquisa de campo e método exigem uma referência de objeto referente ao tipo de declaração. Por convenção, isso é realizado em um class_ref campo:

static IntPtr class_ref = JNIEnv.FindClass(CLASS);

Consulte a seção Referências de Tipo JNI para obter detalhes sobre o CLASS token.

Campos de Ligação

Os campos Java são expostos como propriedades C#, por exemplo, o campo Java java.lang.System.in é associado como a propriedade C# Java.Lang.JavaSystem.In. Além disso, como o JNI distingue entre campos estáticos e campos de instância, diferentes métodos podem ser usados ao implementar as propriedades.

A associação de campo envolve três conjuntos de métodos:

  1. O método get field id . O método get field id é responsável por retornar um identificador de campo que os métodos get field value e set field value usarão. A obtenção do id do campo requer o conhecimento do tipo de declaração, o nome do campo e a assinatura do tipo JNI do campo.

  2. Os métodos get field value . Esses métodos exigem o identificador de campo e são responsáveis por ler o valor do campo de Java. O método a ser usado depende do tipo do campo.

  3. Os métodos de valor de campo definido. Esses métodos exigem o identificador de campo e são responsáveis por gravar o valor do campo em Java. O método a ser usado depende do tipo do campo.

Os campos estáticos usam os métodos JNIEnv.GetStaticFieldIDJNIEnv.GetStatic*Field e JNIEnv.SetStaticField.

Os campos de instância usam os métodos JNIEnv.GetFieldIDJNIEnv.Get*Field e JNIEnv.SetField.

Por exemplo, a propriedade JavaSystem.In static pode ser implementada como:

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

Observação: estamos usando InputStreamInvoker.FromJniHandle para converter a referência JNI em uma System.IO.Stream instância e estamos usando JniHandleOwnership.TransferLocalRef porque JNIEnv.GetStaticObjectField retorna uma referência local.

Muitos dos tipos Android.Runtime têm FromJniHandle métodos que converterão uma referência JNI no tipo desejado.

Vinculação de método

Os métodos Java são expostos como métodos C# e como propriedades C#. Por exemplo, o método Java java.lang.Runtime.runFinalizersOnExit é ligado como o método Java.Lang.Runtime.RunFinalizersOnExit e o método java.lang.Object.getClass é ligado como a propriedade Java.Lang.Object.Class.

A invocação do método é um processo de duas etapas:

  1. A ID do método get para o método a ser invocado. O método get method id é responsável por retornar um identificador de método que os métodos de invocação de método usarão. A obtenção da ID do método requer o conhecimento do tipo de declaração, o nome do método e a assinatura do tipo JNI do método.

  2. Invoque o método.

Assim como acontece com os campos, os métodos a serem usados para obter o id do método e invocá-lo diferem entre métodos estáticos e métodos de instância.

Os métodos estáticos usam JNIEnv.GetStaticMethodID() para pesquisar a ID do método e usam a JNIEnv.CallStatic*Method família de métodos para invocação.

Os métodos de instância usam JNIEnv.GetMethodID para pesquisar a ID do método e usam as JNIEnv.Call*Method famílias de métodos e JNIEnv.CallNonvirtual*Method para invocação.

A associação de método é potencialmente mais do que apenas a invocação de método. A associação de métodos também inclui permitir que um método seja substituído (para métodos abstratos e não finais) ou implementado (para métodos de interface). A seção Herança de Suporte, Interfaces aborda as complexidades do suporte a métodos virtuais e métodos de interface.

Métodos estáticos

A associação de um método estático envolve o uso JNIEnv.GetStaticMethodID de para obter um identificador de método e, em seguida, o uso do método apropriado JNIEnv.CallStatic*Method , dependendo do tipo de retorno do método. Veja a seguir um exemplo de uma associação para o método 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);
}

Observe que armazenamos o identificador do método em um campo estático, id_getRuntime. Essa é uma otimização de desempenho, para que o identificador do método não precise ser pesquisado em cada invocação. Não é necessário armazenar em cache o identificador do método dessa maneira. Depois que o identificador de método é obtido, JNIEnv.CallStaticObjectMethod é usado para chamar o método. JNIEnv.CallStaticObjectMethod retorna um IntPtr que contém o identificador da instância Java retornada. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) é usado para converter o identificador Java em uma instância de objeto fortemente tipada.

Associação de método de instância não virtual

A associação de um final método de instância, ou um método de instância que não requer substituição, envolve o uso JNIEnv.GetMethodID de para obter um identificador de método e, em seguida, o uso do método apropriado JNIEnv.Call*Method , dependendo do tipo de retorno do método. Veja a seguir um exemplo de uma associação para a Object.Class propriedade:

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

Observe que armazenamos o identificador do método em um campo estático, id_getClass. Essa é uma otimização de desempenho, para que o identificador do método não precise ser pesquisado em cada invocação. Não é necessário armazenar em cache o identificador do método dessa maneira. Depois que o identificador de método é obtido, JNIEnv.CallStaticObjectMethod é usado para chamar o método. JNIEnv.CallStaticObjectMethod retorna um IntPtr que contém o identificador da instância Java retornada. Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) é usado para converter o identificador Java em uma instância de objeto fortemente tipada.

Construtores de associação

Construtores são métodos Java com o nome "<init>". Assim como com os métodos de instância Java, JNIEnv.GetMethodID é usado para consultar o identificador do construtor. Ao contrário dos métodos Java, os métodos JNIEnv.NewObject são usados para chamar o identificador do método construtor. O valor retornado de é uma referência local JNI JNIEnv.NewObject :

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…

Normalmente, uma ligação de classe subclasse Java.Lang.Object. Ao criar Java.Lang.Objectuma subclasse , uma semântica adicional entra em jogo: uma Java.Lang.Object instância mantém uma referência global a uma instância Java por meio da Java.Lang.Object.Handle propriedade.

  1. O Java.Lang.Object construtor padrão alocará uma instância Java.

  2. Se o tipo tiver um RegisterAttribute , e RegisterAttribute.DoNotGenerateAcw for true , uma instância do RegisterAttribute.Name tipo será criada por meio de seu construtor padrão.

  3. Caso contrário, o Android Callable Wrapper (ACW) correspondente a this.GetType será instanciado por meio de seu construtor padrão. Os wrappers que podem ser chamados do Android são gerados durante a criação do pacote para cada subclasse para a Java.Lang.Object qual RegisterAttribute.DoNotGenerateAcw não está definido como true.

Para tipos que não são associações de classe, essa é a semântica esperada: instanciar uma Mono.Samples.HelloWorld.HelloAndroid instância C# deve construir uma instância Java mono.samples.helloworld.HelloAndroid que é um Android Callable Wrapper gerado.

Para ligações de classe, esse pode ser o comportamento correto se o tipo Java contiver um construtor padrão e/ou nenhum outro construtor precisar ser chamado. Caso contrário, um construtor deve ser fornecido que execute as seguintes ações:

  1. Chamando o Java.Lang.Object(IntPtr, JniHandleOwnership) em vez do construtor padrão Java.Lang.Object . Isso é necessário para evitar a criação de uma nova instância Java.

  2. Verifique o valor de Java.Lang.Object.Handle antes de criar qualquer instância Java. A Object.Handle propriedade terá um valor diferente de IntPtr.Zero se um Android Callable Wrapper foi construído em código Java e a associação de classe está sendo construída para conter a instância criada do Android Callable Wrapper. Por exemplo, quando o Android cria uma mono.samples.helloworld.HelloAndroid instância, o Android Callable Wrapper será criado primeiro e o construtor Java HelloAndroid criará uma instância do tipo correspondente Mono.Samples.HelloWorld.HelloAndroid , com a Object.Handle propriedade sendo definida como a instância Java antes da execução do construtor.

  3. Se o tipo de runtime atual não for o mesmo que o tipo de declaração, uma instância do Android Callable Wrapper correspondente deverá ser criada e usar Object.SetHandle para armazenar o identificador retornado por JNIEnv.CreateInstance.

  4. Se o tipo de tempo de execução atual for o mesmo que o tipo de declaração, chame o construtor Java e use Object.SetHandle para armazenar o identificador retornado por JNIEnv.NewInstance .

Por exemplo, considere o construtor java.lang.Integer(int). Isso é vinculado como:

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

Os métodos JNIEnv.CreateInstance são auxiliares para executar um JNIEnv.FindClass, JNIEnv.GetMethodID, JNIEnv.NewObjecte JNIEnv.DeleteGlobalReference no valor retornado de JNIEnv.FindClass. Confira a próxima seção para saber mais detalhes.

Suporte a herança, interfaces

A subclasse de um tipo Java ou a implementação de uma interface Java requer a geração de Android Callable Wrappers (ACWs) que são gerados para cada Java.Lang.Object subclasse durante o processo de empacotamento. A geração do ACW é controlada por meio do atributo personalizado Android.Runtime.RegisterAttribute .

Para tipos C#, o construtor de [Register] atributo personalizado requer um argumento: a referência de tipo simplificado JNI para o tipo Java correspondente. Isso permite fornecer nomes diferentes entre Java e C#.

Antes do Xamarin.Android 4.0, o [Register] atributo personalizado não estava disponível para tipos Java existentes de "alias". Isso ocorre porque o processo de geração de ACW geraria ACWs para cada Java.Lang.Object subclasse encontrada.

O Xamarin.Android 4.0 introduziu a propriedade RegisterAttribute.DoNotGenerateAcw . Essa propriedade instrui o processo de geração de ACW a ignorar o tipo anotado, permitindo a declaração de novos Wrappers Callable Gerenciados que não resultarão na geração de ACWs no momento da criação do pacote. Isso permite vincular tipos Java existentes. Por exemplo, considere a seguinte classe Java simples, Adder, que contém um método, add, que adiciona a inteiros e retorna o resultado:

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

O Adder tipo pode ser associado como:

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

Aqui, o Adder tipo C# alias do Adder tipo Java. O [Register] atributo é usado para especificar o nome JNI do mono.android.test.Adder tipo Java e a DoNotGenerateAcw propriedade é usada para inibir a geração de ACW. Isso resultará na geração de um ACW para o ManagedAdder tipo, que subclasse corretamente o mono.android.test.Adder tipo. Se a RegisterAttribute.DoNotGenerateAcw propriedade não tivesse sido usada, o processo de build do Xamarin.Android teria gerado um novo mono.android.test.Adder tipo Java. Isso resultaria em erros de compilação, pois o mono.android.test.Adder tipo estaria presente duas vezes, em dois arquivos separados.

Associando métodos virtuais

ManagedAdder subclasses do tipo Java Adder , mas não é particularmente interessante: o tipo C# Adder não define nenhum método virtual, portanto ManagedAdder , não pode substituir nada.

Os métodos de associação virtual para permitir a substituição por subclasses requerem várias coisas que precisam ser feitas que se enquadram nas duas categorias a seguir:

  1. Vinculação de método

  2. Registro de método

Vinculação de método

Uma associação de método requer a adição de dois membros de suporte à definição de C# Adder : ThresholdType, e ThresholdClass.

Tipo de limite

A ThresholdType propriedade retorna o tipo atual da associação:

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

ThresholdType é usado na associação de método para determinar quando ele deve executar a expedição de método virtual versus não virtual. Ele sempre deve retornar uma System.Type instância que corresponda ao tipo C# de declaração.

Classe de limite

A ThresholdClass propriedade retorna a referência de classe JNI para o tipo vinculado:

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

ThresholdClass é usado na associação de métodos ao invocar métodos não virtuais.

Implementação vinculativa

A implementação da ligação de método é responsável pela chamada de tempo de execução do método Java. Ele também contém uma [Register] declaração de atributo personalizada que faz parte do registro do método e será discutida na seção Registro do Método:

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

O id_add campo contém o ID do método Java a ser chamado. O id_add valor é obtido de JNIEnv.GetMethodID, que requer a classe de declaração (class_ref), o nome do método Java ("add") e a assinatura JNI do método ("(II)I").

Depois que a ID do método é obtida, GetType é comparada para ThresholdType determinar se o despacho virtual ou não virtual é necessário. O despacho virtual é necessário quando GetType corresponde ThresholdTypea , como Handle pode se referir a uma subclasse alocada em Java que substitui o método.

Quando GetType não corresponde a ThresholdType, Adder foi subclasse (por exemplo, por ManagedAdder), e a Adder.Add implementação só será invocada se a subclasse invocar base.Add. Este é o caso de despacho não virtual, que é onde ThresholdClass entra. ThresholdClass especifica qual classe Java fornecerá a implementação do método a ser invocado.

Registro de método

Suponha que temos uma definição atualizada ManagedAdder que substitui o Adder.Add método:

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

Lembre-se de que Adder.Add tinha um [Register] atributo personalizado:

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

O [Register] construtor de atributo personalizado aceita três valores:

  1. O nome do método Java, "add" neste caso.

  2. A assinatura de tipo JNI do método, "(II)I" neste caso.

  3. O método do conector , GetAddHandler neste caso. Os métodos de conector serão discutidos posteriormente.

Os dois primeiros parâmetros permitem que o processo de geração do ACW gere uma declaração de método para substituir o método. O ACW resultante conteria alguns dos seguintes códigos:

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

Observe que um @Override método é declarado, que delega a um n_método prefixado com o mesmo nome. Isso garante que, quando o código Java invocar ManagedAdder.add, ManagedAdder.n_add será invocado, o que permitirá que o método C# ManagedAdder.Add de substituição seja executado.

Assim, a questão mais importante: como é ManagedAdder.n_add ligado a ManagedAdder.Add?

Os métodos Java native são registrados com o tempo de execução Java (o tempo de execução do Android) por meio da função JNI RegisterNatives. RegisterNatives usa uma matriz de estruturas contendo o nome do método Java, a assinatura de tipo JNI e um ponteiro de função para chamar que segue a convenção de chamada JNI. O ponteiro de função deve ser uma função que usa dois argumentos de ponteiro seguidos pelos parâmetros do método. O método Java ManagedAdder.n_add deve ser implementado por meio de uma função que tenha o seguinte protótipo C:

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

O Xamarin.Android não expõe um RegisterNatives método. Em vez disso, o ACW e o MCW juntos fornecem as informações necessárias para invocar RegisterNatives: o ACW contém o nome do método e a assinatura do tipo JNI, a única coisa que falta é um ponteiro de função para conectar.

É aqui que entra o método do conector. O terceiro [Register] parâmetro de atributo personalizado é o nome de um método definido no tipo registrado ou uma classe base do tipo registrado que não aceita parâmetros e retorna um System.Delegate. O retornado System.Delegate , por sua vez, refere-se a um método que tem a assinatura de função JNI correta. Por fim, o delegado que o método do conector retorna deve ser enraizado para que o GC não o colete, pois o delegado está sendo fornecido ao 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

O GetAddHandler método cria um Func<IntPtr, IntPtr, int, int, int> delegado que se refere ao n_Add método e, em seguida, invoca JNINativeWrapper.CreateDelegate. JNINativeWrapper.CreateDelegate encapsula o método fornecido em um bloco try/catch, para que todas as exceções sem tratamento sejam tratadas e resultem na geração do evento AndroidEvent.UnhandledExceptionRaiser . O delegado resultante é armazenado na variável estática cb_add para que o GC não libere o delegado.

Por fim, o n_Add método é responsável por empacotar os parâmetros JNI para os tipos gerenciados correspondentes e, em seguida, delegar a chamada do método.

Observação: sempre use JniHandleOwnership.DoNotTransfer ao obter um MCW em uma instância Java. Tratá-los como uma referência local (e, portanto, chamar JNIEnv.DeleteLocalRef) interromperá as transições de pilha gerenciadas -> Java -> .

Ligação completa do somador

A associação gerenciada completa para o 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
}

Restrições

Ao escrever um tipo que corresponda aos seguintes critérios:

  1. Subclasses Java.Lang.Object

  2. Tem um [Register] atributo personalizado

  3. RegisterAttribute.DoNotGenerateAcw é true

Então, para interação GC, o tipo não deve ter nenhum campo que possa se referir a uma Java.Lang.Object ou Java.Lang.Object subclasse em tempo de execução. Por exemplo, campos de tipo System.Object e qualquer tipo de interface não são permitidos. Tipos que não podem se referir a Java.Lang.Object instâncias são permitidos, como System.String e List<int>. Essa restrição é para evitar a coleta prematura de objetos pelo GC.

Se o tipo deve conter um campo de instância que pode se referir a uma Java.Lang.Object instância, então o tipo de campo deve ser System.WeakReference ou GCHandle.

Métodos abstratos de ligação

Os métodos de ligação abstract são praticamente idênticos aos métodos virtuais de associação. Existem apenas duas diferenças:

  1. O método abstrato é abstrato. Ele ainda retém o [Register] atributo e o Registro de Método associado, a Associação de Método é apenas movida para o Invoker tipo.

  2. Um não-tipo abstract Invoker é criado que subclasse o tipo abstrato. O Invoker tipo deve substituir todos os métodos abstratos declarados na classe base, e a implementação substituída é a implementação de Method Binding, embora o caso de expedição não virtual possa ser ignorado.

Por exemplo, suponha que o método acima mono.android.test.Adder.add fosse abstract. A associação C# seria alterada para que Adder.Add fosse abstrata e um novo AdderInvoker tipo seria definido que implementasse 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));
    }
}

O Invoker tipo só é necessário ao obter referências JNI para instâncias criadas em Java.

Interfaces de ligação

As interfaces de associação são conceitualmente semelhantes às classes de associação que contêm métodos virtuais, mas muitas das especificidades diferem de maneiras sutis (e não tão sutis). Considere a seguinte declaração de interface Java:

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

As associações de interface têm duas partes: a definição de interface C# e uma definição de Invoker para a interface.

Definição de interface

A definição de interface C# deve atender aos seguintes requisitos:

  • A definição de interface deve ter um [Register] atributo personalizado.

  • A definição de interface deve estender o IJavaObject interface. Se isso não for feito, os ACWs herdarão da interface Java.

  • Cada método de interface deve conter um [Register] atributo especificando o nome do método Java correspondente, a assinatura JNI e o método do conector.

  • O método do conector também deve especificar o tipo em que o método do conector pode ser localizado.

Ao associar abstract e virtual métodos, o método do conector seria pesquisado dentro da hierarquia de herança do tipo que está sendo registrado. As interfaces não podem ter métodos que contenham corpos, portanto, isso não funciona, portanto, o requisito de que um tipo seja especificado indicando onde o método do conector está localizado. O tipo é especificado dentro da cadeia de caracteres do método do conector, após dois-pontos ':', e deve ser o nome do tipo qualificado do assembly do tipo que contém o invocador.

As declarações de método de interface são uma conversão do método Java correspondente usando tipos compatíveis . Para tipos internos Java, os tipos compatíveis são os tipos C# correspondentes, por exemplo, Java int é C# int. Para tipos de referência, o tipo compatível é um tipo que pode fornecer um identificador JNI do tipo Java apropriado.

Os membros da interface não serão invocados diretamente pelo Java – a chamada será mediada pelo tipo Invoker – portanto, alguma flexibilidade é permitida.

A interface Java Progress pode ser declarada em C# como:

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

Observe acima que mapeamos o parâmetro Java int[] para um JavaArray<int>. Isso não é necessário: poderíamos tê-lo vinculado a um C# int[], ou um IList<int>, ou algo totalmente diferente. Qualquer que seja o tipo escolhido, é Invoker necessário ser capaz de traduzi-lo em um tipo Java int[] para invocação.

Definição de Invoker

A Invoker definição de tipo deve herdar Java.Lang.Object, implementar a interface apropriada e fornecer todos os métodos de conexão referenciados na definição da interface. Há mais uma sugestão que difere de uma associação de classe: as IDs de class_ref campo e método devem ser membros da instância, não membros estáticos.

O motivo para preferir membros da instância tem a ver com JNIEnv.GetMethodID o comportamento no ambiente de execução do Android. (Isso também pode ser um comportamento do Java; não foi testado.) JNIEnv.GetMethodID Retorna NULL ao pesquisar um método que vem de uma interface implementada e não da interface declarada. Considere a interface Java java.util.SortedMap<K, V> , que implementa a interface java.util.Map<K, V> . Map fornece um método claro , portanto, uma definição aparentemente razoável Invoker para SortedMap seria:

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

O acima falhará porque JNIEnv.GetMethodID retornará null ao procurar o Map.clear método por meio da instância da SortedMap classe.

Há duas soluções para isso: rastrear de qual interface cada método vem e ter um class_ref para cada interface ou manter tudo como membros da instância e executar a pesquisa de método no tipo de classe mais derivado, não no tipo de interface. Este último é feito em Mono.Android.dll.

A definição do Invoker tem seis seções: o construtor, o Dispose método, os ThresholdType membros and ThresholdClass , o método, a GetObject implementação do método de interface e a implementação do método do conector.

Construtor

O construtor precisa pesquisar a classe de tempo de execução da instância que está sendo invocada e armazenar a classe de tempo de execução no campo de instância 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);
    }
}

Observação: a Handle propriedade deve ser usada no corpo do construtor, e não no handle parâmetro, pois no Android v4.0 o handle parâmetro pode ser inválido após a conclusão da execução do construtor base.

Método Dispose

O Dispose método precisa liberar a referência global alocada no construtor:

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

Os ThresholdType membros e ThresholdClass são idênticos ao que é encontrado em uma associação de classe:

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

Método GetObject

Um método estático GetObject é necessário para suportar Extensions.JavaCast<T>():

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

Métodos de interface

Cada método da interface precisa ter uma implementação, que invoca o método Java correspondente por meio do 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étodos de conector

Os métodos de conector e a infraestrutura de suporte são responsáveis por empacotar os parâmetros JNI para os tipos C# apropriados. O parâmetro Java int[] será passado como um JNI jintArray, que é um IntPtr dentro do C#. O IntPtr deve ser empacotado para um JavaArray<int> para dar suporte à invocação da 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);
        }
    }
}

Se int[] preferir , JavaList<int>então JNIEnv.GetArray() pode ser usado em seu lugar:

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

Observe, no entanto, que JNIEnv.GetArray copia toda a matriz entre VMs, portanto, para matrizes grandes, isso pode resultar em muita pressão de GC adicional.

Definição completa do Invoker

A definição completa de IAdderProgressInvoker:

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
}

Referências de objeto JNI

Muitos métodos JNIEnv retornam referências de objeto JNI, que são semelhantes a GCHandles. O JNI fornece três tipos diferentes de referências de objetos: referências locais, referências globais e referências globais fracas. Todos os três são representados como System.IntPtr, mas (de acordo com a seção Tipos de função JNI) nem todos os IntPtrs retornados dos JNIEnv métodos são referências. Por exemplo, JNIEnv.GetMethodID retorna um IntPtr, mas não retorna uma referência de objeto, ele retorna um jmethodID. Consulte a documentação da função JNI para obter detalhes.

As referências locais são criadas pela maioria dos métodos de criação de referência. O Android permite apenas a existência de um número limitado de referências locais a qualquer momento, geralmente 512. As referências locais podem ser excluídas por meio de JNIEnv.DeleteLocalRef. Ao contrário do JNI, nem todos os métodos JNIEnv de referência que retornam referências de objeto retornam referências locais; JNIEnv.FindClass retorna uma referência global . É altamente recomendável que você exclua referências locais o mais rápido possível, possivelmente construindo um Java.Lang.Object em torno do objeto e especificando JniHandleOwnership.TransferLocalRef para o construtor Java.Lang.Object(identificador IntPtr, transferência JniHandleOwnership).

As referências globais são criadas por JNIEnv.NewGlobalRef e JNIEnv.FindClass. Eles podem ser destruídos com JNIEnv.DeleteGlobalRef. Os emuladores têm um limite de 2.000 referências globais pendentes, enquanto os dispositivos de hardware têm um limite de cerca de 52.000 referências globais.

Referências globais fracas estão disponíveis apenas no Android v2.2 (Froyo) e posterior. Referências globais fracas podem ser excluídas com JNIEnv.DeleteWeakGlobalRef.

Lidando com referências locais JNI

Os métodos JNIEnv.GetObjectField, JNIEnv.GetStaticObjectField, JNIEnv.CallObjectMethod, JNIEnv.CallNonvirtualObjectMethod e JNIEnv.CallStaticObjectMethod retornam um IntPtr que contém uma referência local JNI a um objeto Java ou IntPtr.Zero , se Java retornou null. Devido ao número limitado de referências locais que podem ser pendentes de uma só vez (512 entradas), é desejável garantir que as referências sejam excluídas em tempo hábil. Há três maneiras de lidar com as referências locais: excluí-las explicitamente, criar uma Java.Lang.Object instância para mantê-las e usá-las Java.Lang.Object.GetObject<T>() para criar um wrapper que pode ser chamado gerenciado em torno delas.

Excluindo explicitamente referências locais

JNIEnv.DeleteLocalRef é usado para excluir referências locais. Uma vez que a referência local tenha sido excluída, ela não pode mais ser usada, portanto, deve-se tomar cuidado para garantir que essa JNIEnv.DeleteLocalRef seja a última coisa feita com a referência local.

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

Encapsulamento com Java.Lang.Object

Java.Lang.Object fornece um construtor Java.Lang.Object(IntPtr handle, JniHandleOwnership transfer) que pode ser usado para encapsular uma referência JNI existente. O parâmetro JniHandleOwnership determina como o IntPtr parâmetro deve ser tratado:

  • JniHandleOwnership.DoNotTransfer – A instância criada Java.Lang.Object criará uma nova referência global a partir do handle parâmetro e handle não será alterada. O chamador é responsável por liberar handle , se necessário.

  • JniHandleOwnership.TransferLocalRef – A instância criada Java.Lang.Object criará uma nova referência global a handle partir do parâmetro e handle será excluída com JNIEnv.DeleteLocalRef . O chamador não deve liberar handle e não deve usar handle depois que o construtor terminar de executar.

  • JniHandleOwnership.TransferGlobalRef – A instância criada Java.Lang.Object assumirá a handle propriedade do parâmetro. O chamador não deve liberar handle .

Como os métodos de invocação do método JNI retornam refs locais, JniHandleOwnership.TransferLocalRef normalmente seriam usados:

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

A referência global criada não será liberada até que a instância seja coletada Java.Lang.Object como lixo. Se você puder, descartar a instância liberará a referência global, acelerando as coletas de lixo:

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

Usando Java.Lang.Object.GetObject<T>()

Java.Lang.Object fornece um método Java.Lang.Object.GetObject<T>(identificador IntPtr, transferência JniHandleOwnership) que pode ser usado para criar um wrapper que pode ser chamado gerenciado do tipo especificado.

O tipo T deve atender aos seguintes requisitos:

  1. T deve ser um tipo de referência.

  2. T deve implementar a IJavaObject interface.

  3. Se T não for uma classe ou interface abstrata, deverá T fornecer um construtor com os tipos de parâmetro.(IntPtr, JniHandleOwnership)

  4. Se T for uma classe abstrata ou uma interface, deve haver um invocador disponível para T . Um invocador é um tipo não abstrato que herda T ou implementa T e tem o mesmo nome de T um sufixo Invoker. Por exemplo, se T for a interface Java.Lang.IRunnable , o tipo Java.Lang.IRunnableInvoker deverá existir e conter o construtor necessário (IntPtr, JniHandleOwnership) .

Como os métodos de invocação do método JNI retornam refs locais, JniHandleOwnership.TransferLocalRef normalmente seriam usados:

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

Pesquisando tipos Java

Para pesquisar um campo ou método no JNI, o tipo de declaração do campo ou método deve ser pesquisado primeiro. O método Android.Runtime.JNIEnv.FindClass(string)) é usado para pesquisar tipos Java. O parâmetro string é a referência de tipo simplificada ou a referência de tipo completo para o tipo Java. Consulte a seção Referências de tipo JNI para obter detalhes sobre referências de tipo simplificadas e completas.

Nota: Ao contrário de qualquer outro JNIEnv método que retorna instâncias de objeto, FindClass retorna uma referência global, não uma referência local.

Campos de instância

Os campos são manipulados por meio de IDs de campo. As IDs de campo são obtidas por meio de JNIEnv.GetFieldID, que requer a classe na qual o campo está definido, o nome do campo e a assinatura de tipo JNI do campo.

Os IDs de campo não precisam ser liberados e são válidos desde que o tipo Java correspondente seja carregado. (No momento, o Android não dá suporte ao descarregamento de classes.)

Há dois conjuntos de métodos para manipular campos de instância: um para ler campos de instância e outro para gravar campos de instância. Todos os conjuntos de métodos exigem uma ID de campo para ler ou gravar o valor do campo.

Lendo valores de campo de instância

O conjunto de métodos para ler valores de campo de instância segue o padrão de nomenclatura:

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

onde * é o tipo do campo:

Escrevendo valores de campo de instância

O conjunto de métodos para escrever valores de campo de instância segue o padrão de nomenclatura:

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

onde Tipo é o tipo do campo:

  • JNIEnv.SetField) – Escreva o valor de qualquer campo que não seja um tipo interno, como java.lang.Object matrizes e tipos de interface. O IntPtr valor pode ser uma referência local JNI, referência global JNI, referência global JNI fraca ou IntPtr.Zero (for null ).

  • JNIEnv.SetField) – Escreva o valor dos campos de bool instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de sbyte instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de char instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de short instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de int instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de long instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de float instância.

  • JNIEnv.SetField) – Escreva o valor dos campos de double instância.

Campos estáticos

Os campos estáticos são manipulados por meio de IDs de campo. As IDs de campo são obtidas por meio de JNIEnv.GetStaticFieldID, que requer a classe na qual o campo está definido, o nome do campo e a assinatura de tipo JNI do campo.

Os IDs de campo não precisam ser liberados e são válidos desde que o tipo Java correspondente seja carregado. (No momento, o Android não dá suporte ao descarregamento de classes.)

Há dois conjuntos de métodos para manipular campos estáticos: um para ler campos de instância e outro para gravar campos de instância. Todos os conjuntos de métodos exigem uma ID de campo para ler ou gravar o valor do campo.

Lendo valores de campo estático

O conjunto de métodos para ler valores de campo estático segue o padrão de nomenclatura:

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

onde * é o tipo do campo:

Escrevendo valores de campo estático

O conjunto de métodos para escrever valores de campo estático segue o padrão de nomenclatura:

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

onde Tipo é o tipo do campo:

Métodos de instância

Os métodos de instância são invocados por meio de IDs de método. As IDs de método são obtidas por meio de JNIEnv.GetMethodID, que requer o tipo no qual o método está definido, o nome do método e a assinatura de tipo JNI do método.

Os IDs de método não precisam ser liberados e são válidos desde que o tipo Java correspondente seja carregado. (No momento, o Android não dá suporte ao descarregamento de classes.)

Há dois conjuntos de métodos para invocar métodos: um para invocar métodos virtualmente e outro para invocar métodos não virtualmente. Ambos os conjuntos de métodos exigem uma ID de método para chamar o método, e a invocação não virtual também requer que você especifique qual implementação de classe deve ser invocada.

Os métodos de interface só podem ser pesquisados dentro do tipo de declaração; Os métodos provenientes de interfaces estendidas/herdadas não podem ser pesquisados. Consulte a seção posterior Interfaces de Associação/Implementação do Invoker para obter mais detalhes.

Qualquer método declarado na classe ou em qualquer classe base ou interface implementada pode ser pesquisado.

Invocação de método virtual

O conjunto de métodos para invocar métodos segue virtualmente o padrão de nomenclatura:

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

onde * é o tipo de retorno do método.

Invocação de método não virtual

O conjunto de métodos para invocar métodos não virtualmente segue o padrão de nomenclatura:

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

onde * é o tipo de retorno do método. A invocação de método não virtual geralmente é usada para invocar o método base de um método virtual.

Métodos estáticos

Os métodos estáticos são invocados por meio de IDs de método. As IDs de método são obtidas por meio de JNIEnv.GetStaticMethodID, que requer o tipo em que o método está definido, o nome do método e a assinatura de tipo JNI do método.

Os IDs de método não precisam ser liberados e são válidos desde que o tipo Java correspondente seja carregado. (No momento, o Android não dá suporte ao descarregamento de classes.)

Invocação de método estático

O conjunto de métodos para invocar métodos segue virtualmente o padrão de nomenclatura:

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

onde * é o tipo de retorno do método.

Assinaturas de tipo JNI

As assinaturas de tipo JNI são referências de tipo JNI (embora não sejam referências de tipo simplificadas), exceto para métodos. Com métodos, a assinatura de tipo JNI é um parêntese '('aberto, seguido pelas referências de tipo para todos os tipos de parâmetros concatenados (sem vírgulas de separação ou qualquer outra coisa), seguido por um parêntese ')'de fechamento, seguido pela referência de tipo JNI do tipo de retorno do método.

Por exemplo, dado o método Java:

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

A assinatura de tipo JNI seria:

(ILjava/lang/String;[I)J

Em geral, é altamente recomendável usar o javap comando para determinar assinaturas JNI. Por exemplo, a assinatura de tipo JNI do método java.lang.Thread.State.valueOf(String) é "(Ljava/lang/String;)Ljava/lang/Thread$State;", enquanto a assinatura de tipo JNI do método java.lang.Thread.State.values é "()[Ljava/lang/Thread$State;". Cuidado com os pontos e vírgulas à direita; eles fazem parte da assinatura do tipo JNI.

Referências de tipo JNI

As referências de tipo JNI são diferentes das referências de tipo Java. Você não pode usar nomes de tipo Java totalmente qualificados, como java.lang.String com JNI, você deve usar as variações "java/lang/String" JNI ou "Ljava/lang/String;", dependendo do contexto; veja abaixo para obter detalhes. Existem quatro tipos de referências de tipo JNI:

  • incorporado
  • Simplificado
  • tipo
  • array

Referências de tipo internas

As referências de tipo interno são um único caractere, usado para fazer referência a tipos de valor internos. O mapeamento é o seguinte:

  • "B" para sbyte .
  • "S" para short .
  • "I" para int .
  • "J" para long .
  • "F" para float .
  • "D" para double .
  • "C" para char .
  • "Z" para bool .
  • "V" para tipos de retorno de void método.

Referências de tipo simplificadas

As referências de tipo simplificadas só podem ser usadas em JNIEnv.FindClass(string)). Há duas maneiras de derivar uma referência de tipo simplificada:

  1. A partir de um nome Java totalmente qualificado, substitua every '.' dentro do nome do pacote e antes do nome do tipo por '/' , e every '.' dentro de um nome de tipo por '$' .

  2. Leia a saída do 'unzip -l android.jar | grep JavaName' .

Qualquer um dos dois resultará no mapeamento do tipo Java java.lang.Thread.State para a referência java/lang/Thread$Statede tipo simplificado.

Referências de tipo

Uma referência de tipo é uma referência de tipo interna ou uma referência de tipo simplificada com um 'L' prefixo e um ';' sufixo. Para o tipo Java java.lang.String, a referência de tipo simplificado é "java/lang/String", enquanto a referência de tipo é "Ljava/lang/String;".

As referências de tipo são usadas com referências de tipo de matriz e com assinaturas JNI.

Uma maneira adicional de obter uma referência de tipo é lendo a saída de 'javap -s -classpath android.jar fully.qualified.Java.Name'. Dependendo do tipo envolvido, você pode usar uma declaração de construtor ou um tipo de retorno de método para determinar o nome JNI. Por exemplo:

$ 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 é um tipo de enumeração Java, portanto, podemos usar a assinatura do método para determinar que a referência de valueOf tipo é Ljava/lang/Thread$State;.

Referências de tipo de matriz

As referências de tipo de matriz são '[' prefixadas a uma referência de tipo JNI. Referências de tipo simplificadas não podem ser usadas ao especificar matrizes.

Por exemplo, int[] é "[I", int[][] é "[[I", e java.lang.Object[] é "[Ljava/lang/Object;".

Java Genéricos e apagamento de tipo

Na maioria das vezes, como visto através do JNI, os genéricos Java não existem. Existem algumas "rugas", mas essas rugas estão em como o Java interage com os genéricos, não em como o JNI procura e invoca membros genéricos.

Não há diferença entre um tipo ou membro genérico e um tipo ou membro não genérico ao interagir por meio do JNI. Por exemplo, o tipo genérico java.lang.Class<T> também é o tipo java.lang.Classgenérico "bruto" , ambos com a mesma referência de tipo simplificado, "java/lang/Class".

Suporte à interface nativa Java

Android.Runtime.JNIEnv é um wrapper gerenciado para a JNI (Interface Nativa do Jave). As funções JNI são declaradas dentro da Especificação de Interface Nativa Java, embora os métodos tenham sido alterados para remover o parâmetro explícito JNIEnv* e IntPtr sejam usados em vez de jobject, jclass, jmethodID, etc. Por exemplo, considere a função JNI NewObject:

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

Isso é exposto como o método JNIEnv.NewObject :

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

A tradução entre as duas chamadas é razoavelmente simples. Em C você teria:

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

O equivalente em C# seria:

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

Depois de ter uma instância de objeto Java mantida em um IntPtr, você provavelmente desejará fazer algo com ela. Você pode usar métodos JNIEnv, como JNIEnv.CallVoidMethod() para fazer isso, mas se já houver um wrapper C# analógico, você desejará construir um wrapper sobre a referência JNI. Você pode fazer isso por meio do método de extensão 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>();

Você também pode usar o método T Java.Lang.Object.GetObject><:

IntPtr lrefActivity = CreateMapActivity();

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

Além disso, todas as funções JNI foram modificadas removendo o JNIEnv* parâmetro presente em todas as funções JNI.

Resumo

Lidar diretamente com a JNI é uma experiência terrível que deve ser evitada a todo custo. Infelizmente, nem sempre é evitável; espero que este guia forneça alguma ajuda quando você acertar os casos Java não vinculados com Mono para Android.