Share 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 fornece associações para bibliotecas Java, incluindo Mono.Android.dll e Mono.Android.GoogleMaps.dll. No entanto, as associações não são fornecidas para todas as bibliotecas Java possíveis e as associações fornecidas podem não associar todos os tipos e membros Java. Para usar tipos e membros Java não associados, o JNI (Java Native Interface) pode ser usado. Este artigo ilustra como usar o JNI para interagir com tipos Java e membros de aplicativos Xamarin.Android.

Visão geral

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

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

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

Este documento explica:

  • Como o JNI se refere aos 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

O 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 callable gerenciados

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

Os wrappers callable gerenciados atendem a duas finalidades:

  1. Encapsular o uso de JNI para que o código do cliente não precise saber sobre a complexidade subjacente.
  2. Possibilite a subclasse tipos Java e implemente interfaces Java.

A primeira finalidade é puramente para conveniência e encapsulamento de complexidade para que os consumidores tenham um conjunto simples e gerenciado de classes a ser usado. Isso requer o uso dos vários membros JNIEnv , conforme descrito posteriormente neste artigo. Tenha em mente 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 associados. A implementação de subclasse e interface requer o uso de wrappers callable gerenciados.

Callable Wrappers do Android

Os WRAPPERs callable do Android (ACW) são necessários sempre que o ART (runtime do Android) precisa invocar o código gerenciado; esses wrappers são necessários porque não há como registrar classes com ART em runtime. (Especificamente, a função DefineClass JNI não é compatível com o runtime do Android. Os wrappers callable do Android, portanto, compõem a falta de suporte ao registro de tipo de runtime.)

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

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

Implementando interfaces

Há momentos em que talvez seja 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 IJavaObject. O Xamarin.Android aproveita esse fato – ele usa IJavaObject para fornecer ao Android um proxy Java (um wrapper callable do Android) para o tipo gerenciado fornecido. Como monodroid.exe procura Java.Lang.Object apenas subclasses (que devem implementar IJavaObject), a subclasse Java.Lang.Object nos fornece uma maneira de implementar interfaces no 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 sob os 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 programamandroid.exe gerará o seguinte Wrapper Callable do Android:

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 se baseia 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 dá suporte a nomes de ação em atributos XML de layout, por exemplo, o atributo XML android:onClick . Quando é especificada, a instância de Exibição inflada tenta pesquisar o método Java.

  • A interface java.io.Serializable requer métodos readObject 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 o campo no código Java 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 Xamarin.Android 4.2, exportAttribute e ExportFieldAttribute foram introduzidos para oferecer uma solução para os cenários acima. Ambos os atributos residem no Java.Interop 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 expedição para a invocação 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 deMono.Android.Export.dll – se você usou ExportAttribute ou ExportFieldAttribute em alguns métodos em seu código ou bibliotecas dependentes, precisará adicionar Mono.Android.Export.dll. Esse assembly é isolado para dar suporte ao código de retorno de chamada do Java. Ele é separado de Mono.Android.dll , pois adiciona tamanho adicional ao aplicativo.

  • Em Build de versão, MissingMethodException ocorre para métodos de exportação – No build 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 fornecem a funcionalidade que o código de tempo de execução java pode usar. Esse código em tempo de execução acessa o código gerenciado por meio dos métodos JNI gerados controlados 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, esse caso não é totalmente determinante. Mais notavelmente, isso é verdadeiro em alguns mapeamentos avançados entre tipos gerenciados e tipos Java, como:

  • InputStream
  • Outputstream
  • XmlPullParser
  • XmlResourceParser

Quando tipos como esses são necessários para métodos exportados, o ExportParameterAttribute deve ser usado para dar 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ção em wrappers Java.

Isso significa as seguintes alterações direcionais:

  • O gerador de associação gera Java.Lang.DeprecatedAttribute de java.Lang.Deprecated (enquanto deve estar [Obsolete] no 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 habituais (se esse uso existir). Haverá Deprecated classes e DeprecatedAttribute .

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

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

Se a classe que contém (a própria classe anotada ou a classe que contém os membros anotados) não estiver 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 origem Java que define 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é agora.

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

Associação de classe

Associar uma classe significa escrever um wrapper callable gerenciado para simplificar a invocação do tipo Java subjacente.

Associar 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 identificador de tipo

Os métodos de pesquisa de campo e método exigem uma referência de objeto que se refere 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 associação

Os campos Java são expostos como propriedades C#, por exemplo, o campo java java.lang.System.in está 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, métodos diferentes serão 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. Obter a ID do campo requer saber o 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 do Java. O método a ser usado depende do tipo do campo.

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

Os campos estáticos usam os métodos JNIEnv.GetStaticFieldID, JNIEnv.GetStatic*Fielde JNIEnv.SetStaticField .

Os campos de instância usam os métodos JNIEnv.GetFieldID, JNIEnv.Get*Fielde JNIEnv.SetField .

Por exemplo, a propriedade JavaSystem.In estática 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.

Associaçã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 é associado como o método Java.Lang.Runtime.RunFinalizersOnExit e o método java.lang.Object.getClass é associado como a propriedade Java.Lang.Object.Class .

A invocação de 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. Obter a ID do método requer saber o tipo de declaração, o nome do método e a assinatura de tipo JNI do método.

  2. Invoque o método.

Assim como acontece com os campos, os métodos a serem usados para obter a ID do método e invocar o método 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 usar 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 usar as JNIEnv.Call*Method famílias e JNIEnv.CallNonvirtual*Method dos métodos para invocação.

A associação de método é potencialmente mais do que apenas invocação de método. A associação de método 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

Associar um método estático envolve o uso JNIEnv.GetStaticMethodID de para obter um identificador de método e, em seguida, usar o 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 de 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 de método dessa maneira. Depois que o identificador do método é obtido, JNIEnv.CallStaticObjectMethod é usado para invocar 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

Associar 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, usar o 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 de 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 de método dessa maneira. Depois que o identificador do método é obtido, JNIEnv.CallStaticObjectMethod é usado para invocar 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 acontece com os métodos de instância Java, JNIEnv.GetMethodID é usado para pesquisar o identificador do construtor. Ao contrário dos métodos Java, os métodos JNIEnv.NewObject são usados para invocar o identificador de método do construtor. O valor retornado de JNIEnv.NewObject é uma referência local de 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…

Normalmente, uma associação de classe subclasse Java.Lang.Object. Ao subclasse Java.Lang.Object, 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 ACW ( Android Callable Wrapper ) correspondente a this.GetType é instanciado por meio de seu construtor padrão. Os Wrappers Callable do Android são gerados durante a criação do pacote para cada Java.Lang.Object subclasse para a qual RegisterAttribute.DoNotGenerateAcw não está definido truecomo .

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

Para associações de classe, esse poderá ser o comportamento correto se o tipo Java contiver um construtor padrão e/ou nenhum outro construtor precisar ser invocado. Caso contrário, será necessário fornecer um construtor que execute as seguintes ações:

  1. Invocando 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 instâncias Java. A Object.Handle propriedade terá um valor diferente de IntPtr.Zero se um Wrapper Callable do Android tiver sido construído no código Java e a associação de classe estiver sendo construída para conter a instância de Wrapper Callable do Android criada. 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 Wrapper Callable do Android correspondente deverá ser criada e usar Object.SetHandle para armazenar o identificador retornado por JNIEnv.CreateInstance.

  4. Se o tipo de runtime atual for o mesmo que o tipo de declaração, invoque 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 é associado 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 à herança, interfaces

A subclasse de um tipo Java ou a implementação de uma interface Java exige a geração de ACWs (Wrappers Callable ) do Android 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 [Register] construtor de atributo personalizado requer um argumento: a referência de tipo simplificado de 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 do 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 do 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 associar 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# aliases o 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 do 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.

Métodos virtuais de associação

ManagedAdder subclasse o 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 virtual métodos de associação para permitir a substituição por subclasses exigem várias coisas que precisam ser feitas que se enquadram nas duas categorias a seguir:

  1. Associação de método

  2. Registro de método

Associação de método

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

ThresholdType

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

ThresholdClass

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

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

ThresholdClass é usado na Associação de Método ao invocar métodos não virtuais.

Implementação de associação

A implementação da associação de método é responsável pela invocação de runtime do método Java. Ele também contém uma [Register] declaração de atributo personalizado que faz parte do registro do método e será discutida na seção Registro de 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 a ID do método para o método Java a ser invocado. 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 é comparado a ThresholdType para determinar se a expedição virtual ou não virtual é necessária. A expedição virtual é necessária 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 ThresholdTypea , Adder foi subclasse (por exemplo, por ManagedAdder) e a Adder.Add implementação só será invocada se a subclasse tiver invocado base.Add. Esse é o caso de expedição 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 tenhamos uma definição atualizada ManagedAdder que substitua 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" nesse caso.

  2. A Assinatura de Tipo JNI do método , "(II)I" nesse caso.

  3. O método de conector , GetAddHandler nesse caso. Os métodos do 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 de 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.

Portanto, a pergunta mais importante: como é ManagedAdder.n_add conectado a ManagedAdder.Add?

Os métodos Java native são registrados com o runtime do Java (o runtime do Android) por meio da função JNI RegisterNatives. RegisterNatives usa uma matriz de estruturas que contêm o nome do método Java, a Assinatura de Tipo JNI e um ponteiro de função para invocar 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 de tipo JNI, a única coisa que falta é um ponteiro de função para conectar.

É aí 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 conector retorna deve ter raiz 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 invoca JNINativeWrapper.CreateDelegate. JNINativeWrapper.CreateDelegate encapsula o método fornecido em um bloco try/catch, para que quaisquer exceções sem tratamento sejam tratadas e resultarão 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 realizar marshaling dos parâmetros JNI para os tipos gerenciados correspondentes e, em seguida, delegar a chamada de 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 –> gerenciadas.

Associação completa de Adder

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

Em seguida, para interação com o GC, o tipo não deve ter nenhum campo que possa se referir a uma Java.Lang.Object subclasse ou Java.Lang.Object em runtime. Por exemplo, campos do 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 precisar conter um campo de instância que possa se referir a uma Java.Lang.Object instância, o tipo de campo deverá ser System.WeakReference ou GCHandle.

Associando métodos abstratos

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

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

  2. Um tipo não abstractInvoker é 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 Associação de Método, 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 era abstract. A associação C# seria alterada para que Adder.Add fosse abstrata e um novo AdderInvoker tipo fosse definido, o que implementava 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 de JNI para instâncias criadas em Java.

Interfaces de associaçã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 da interface C# e uma definição de Invoker para a interface .

Definição de interface

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

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

  • A definição da interface deve estender o IJavaObject interface. A falha ao fazer isso impedirá que as ACWs herdem da interface Java.

  • Cada método de interface deve conter um [Register] atributo que especifique 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 no qual o método do conector pode ser localizado.

Ao associar abstract e virtual métodos, o método de 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 na cadeia de caracteres do método do conector, após dois-pontos ':', e deve ser o nome de tipo qualificado do assembly do tipo que contém o invocador.

As declarações de método de interface são uma tradução do método Java correspondente usando tipos compatíveis . Para tipos de builtin 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 invocação será mediada por meio do tipo Invoker – portanto, alguma quantidade de flexibilidade é permitida.

A interface Progresso do Java 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 no acima que mapeamos o parâmetro Java int[] para um int> JavaArray<. Isso não é necessário: poderíamos ter associado a um C# int[]ou a um IList<int>ou algo totalmente diferente. Qualquer tipo escolhido, o Invoker precisa ser capaz de convertê-lo em um tipo Java int[] para invocação.

Definição do invocador

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 de método devem ser membros de instância, não membros estáticos.

O motivo para preferir membros de instância tem a ver com JNIEnv.GetMethodID o comportamento no runtime do Android. (Esse também pode ser o comportamento do Java; ele não foi testado.) JNIEnv.GetMethodID retorna nulo ao procurar um método que vem de uma interface implementada e não da interface declarada. Considere a interface java.util.SortedMap<K, V> Java, que implementa a interface java.util.Map<K, V> . O mapa 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 de SortedMap classe.

Há duas soluções para isso: controlar 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 invoker tem seis seções: o construtor, o Dispose método, os ThresholdType membros e ThresholdClass , o método, a GetObject implementação do método de interface e a implementação do método conector.

Construtor

O construtor precisa pesquisar a classe de runtime da instância que está sendo invocada e armazenar a classe de runtime no campo da 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 dar suporte a 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 de 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 do conector e a infraestrutura de suporte são responsáveis por realizar marshaling dos parâmetros JNI para tipos C# apropriados. O parâmetro Java int[] será passado como um JNI jintArray, que é um IntPtr dentro de C#. O IntPtr deve ser empacotado em 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[] fosse preferível em vez JavaList<int>de , JNIEnv.GetArray() poderia ser usado em vez disso:

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

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

Definição completa do invocador

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 objetoJNI, que são semelhantes a GCHandles. O JNI fornece três tipos diferentes de referências de objeto: 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, 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 só permite que um número limitado de referências locais exista 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 excluir 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, JniHandleOwnership transfer).

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 só estão disponíveis no Android v2.2 (Froyo) e posteriores. Referências globais fracas podem ser excluídas com JNIEnv.DeleteWeakGlobalRef.

Lidando com referências locais de 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 ao mesmo tempo (512 entradas), é desejável garantir que as referências sejam excluídas em tempo hábil. Há três maneiras com as quais as referências locais podem ser tratadas: excluí-las explicitamente, criar uma Java.Lang.Object instância para mantê-las e usar Java.Lang.Object.GetObject<T>() para criar um wrapper callable gerenciado ao seu redor.

Excluindo explicitamente referências locais

JNIEnv.DeleteLocalRef é usado para excluir referências locais. Depois que a referência local tiver sido excluída, ela não poderá mais ser usada, portanto, deve-se tomar cuidado para garantir que JNIEnv.DeleteLocalRef essa 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);
}

Encapsulando com Java.Lang.Object

Java.Lang.Object fornece um construtor Java.Lang.Object(Identificador intPtr, JniHandleOwnership transfer), que pode ser usado para encapsular uma referência JNI de saída. 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 com base no handle parâmetro e handle permanecerá inalterada. 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 do handle 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 propriedade do handle 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 seria usado:

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 Java.Lang.Object instância seja coletada. Se você conseguir, o descarte da 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>(IntPtr handle, JniHandleOwnership transfer) que pode ser usado para criar um wrapper callable 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 (IntPtr, JniHandleOwnership) de parâmetro .

  4. Se T for uma classe abstrata ou uma interface, deverá 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 T que com um sufixo Invoker. Por exemplo, se T for a interface Java.Lang.IRunnable , o tipo Java.Lang.IRunnableInvoker deverá existir e deverá conter o construtor necessário (IntPtr, JniHandleOwnership) .

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

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 para o 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 simplificado 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 simplificadas e de tipo completo.

Observação: ao contrário de todos os outros JNIEnv métodos que retornam 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, o que requer a classe na qual o campo está definido, o nome do campo e a Assinatura de Tipo JNI do campo.

As IDs de campo não precisam ser liberadas e são válidas desde que o tipo Java correspondente seja carregado. (No momento, o Android não dá suporte ao descarregamento de classe.)

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.

Valores de campo de instância de leitura

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

em que * é o tipo do campo:

Gravando valores de campo de instância

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

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

em que 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 de JNI, referência global JNI, referência global fraca de JNI ou IntPtr.Zero (para 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

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.

As IDs de campo não precisam ser liberadas e são válidas desde que o tipo Java correspondente seja carregado. (No momento, o Android não dá suporte ao descarregamento de classe.)

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

em que * é o tipo do campo:

Gravando valores de campo estático

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

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

em que 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 é definido, o nome do método e a Assinatura de Tipo JNI do método.

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

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 invocar o método e a invocação não virtual também exige 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; métodos provenientes de interfaces estendidas/herdadas não podem ser pesquisados. Consulte a seção Interfaces de Associação/Implementação do Invocador posterior para obter mais detalhes.

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

Invocação de Método Virtual

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

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

em que * é 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 );

em que * é 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

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 no qual o método é definido, o nome do método e a Assinatura de Tipo JNI do método.

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

Invocação de método estático

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

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

em que * é o tipo de retorno do método .

Assinaturas de tipo JNI

Assinaturas de tipo JNI são referências de tipo JNI (embora não referências de tipo simplificadas), exceto para métodos. Com os métodos, a Assinatura de Tipo JNI é um parêntese '('aberto , seguido pelas referências de tipo para todos os tipos de parâmetro 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, considerando o método Java:

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

A assinatura do 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 ponto e vírgula à direita; elas fazem parte da assinatura de tipo JNI.

Referências de tipo JNI

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

  • interno
  • Simplificado
  • tipo
  • array

Referências de tipo interno

Referências de tipo interno são um único caractere, usado para referenciar 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 void tipos de retorno de método.

Referências simplificadas de tipo

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 cada '.' dentro do nome do pacote e antes do nome do tipo por '/' e cada dentro de '.' um nome de tipo por '$' .

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

Qualquer um dos dois fará com que o tipo Java java.lang.Thread.State seja mapeado para a referência java/lang/Thread$Statede tipo simplificada .

Referências de tipo

Uma referência de tipo é uma referência de tipo interno 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 simplificada é "java/lang/String", enquanto a referência de tipo é "Ljava/lang/String;".

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 valueOf método para determinar que a referência de tipo é Ljava/lang/Thread$State;.

Referências de tipo de matriz

As referências de tipo de matriz são '[' prefixadas em 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 Generics and Type Erasure

Na maioria das vezes, como visto por meio de JNI, os genéricos Java não existem. Há algumas "rugas", mas essas rugas estão em como Java interage com genéricos, não com a forma como o JNI olha para cima 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 de JNI. Por exemplo, o tipo genérico java.lang.Class<T> também é o tipo genérico "bruto", java.lang.Classambos com a mesma referência de tipo simplificado, "java/lang/Class".

Suporte à interface nativa do Java

Android.Runtime.JNIEnv é um wrapper gerenciado para a JNI (Interface Nativa Jave). As Funções JNI são declaradas dentro da Especificação de Interface Nativa do 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, jmethodIDetc. 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);

Traduzir 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 do 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 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);

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

Resumo

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