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:
- Encapsular o uso de JNI para que o código do cliente não precise saber sobre a complexidade subjacente.
- 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
ewriteObject
. 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 tipoParcelable.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 comandroid:onClick
ejava.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 comandroid.os.Parcelable
.
Solução de problemas de ExportAttribute e ExportFieldAttribute
O empacotamento falha devido à falta deMono.Android.Export.dll – se você usou
ExportAttribute
ouExportFieldAttribute
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
dejava.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 eDeprecatedAttribute
.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:
Um identificador JNI para o tipo Java que está sendo associado.
Se a subclasse for necessária, o tipo precisará ter um atributo personalizado RegisterAttribute na declaração de tipo com RegisterAttribute.DoNotGenerateAcw definido
true
como .
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:
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.
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.
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*Field
e JNIEnv.SetStaticField .
Os campos de instância usam os métodos JNIEnv.GetFieldID, JNIEnv.Get*Field
e 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:
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.
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 .
O
Java.Lang.Object
construtor padrão alocará uma instância java.Se o tipo tiver um
RegisterAttribute
eRegisterAttribute.DoNotGenerateAcw
fortrue
, uma instância doRegisterAttribute.Name
tipo será criada por meio de seu construtor padrão.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 cadaJava.Lang.Object
subclasse para a qualRegisterAttribute.DoNotGenerateAcw
não está definidotrue
como .
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:
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.Verifique o valor de Java.Lang.Object.Handle antes de criar instâncias Java. A
Object.Handle
propriedade terá um valor diferente deIntPtr.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 umamono.samples.helloworld.HelloAndroid
instância, o Android Callable Wrapper será criado primeiro e o construtor JavaHelloAndroid
criará uma instância do tipo correspondenteMono.Samples.HelloWorld.HelloAndroid
, com aObject.Handle
propriedade sendo definida como a instância java antes da execução do construtor.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.
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 & 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.NewObject
e 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:
Associação de método
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
: ThresholdType
e 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 ThresholdType
a , como Handle
pode se referir a uma subclasse alocada em Java que substitui o método .
Quando GetType
não corresponde ThresholdType
a , 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:
O nome do método Java,
"add"
nesse caso.A Assinatura de Tipo JNI do método ,
"(II)I"
nesse caso.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:
Subclasses
Java.Lang.Object
Tem um
[Register]
atributo personalizadoRegisterAttribute.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:
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 oInvoker
tipo .Um tipo não
abstract
Invoker
é criado, que subclasse o tipo abstrato. OInvoker
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 GCHandle
s. 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 IntPtr
s 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 nohandle
parâmetro ehandle
permanecerá inalterada. O chamador é responsável por liberarhandle
, se necessário.JniHandleOwnership.TransferLocalRef – a instância criada
Java.Lang.Object
criará uma nova referência global dohandle
parâmetro ehandle
será excluída com JNIEnv.DeleteLocalRef . O chamador não deve liberarhandle
e não deve usarhandle
depois que o construtor terminar de executar.JniHandleOwnership.TransferGlobalRef – a instância criada
Java.Lang.Object
assumirá a propriedade dohandle
parâmetro. O chamador não deve liberarhandle
.
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:
T
deve ser um tipo de referência.T
deve implementar aIJavaObject
interface .Se
T
não for uma classe ou interface abstrata, deveráT
fornecer um construtor com os tipos(IntPtr, JniHandleOwnership)
de parâmetro .Se
T
for uma classe abstrata ou uma interface, deverá haver um invocador disponível paraT
. Um invocador é um tipo não abstrato que herdaT
ou implementaT
e tem o mesmo nomeT
que com um sufixo Invoker. Por exemplo, se T for a interfaceJava.Lang.IRunnable
, o tipoJava.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:
JNIEnv.GetObjectField – leia o valor de qualquer campo de instância que não seja um tipo interno, como
java.lang.Object
, matrizes e tipos de interface. O valor retornado é uma referência local de JNI.JNIEnv.GetBooleanField – Leia o valor dos campos de
bool
instância.JNIEnv.GetByteField – Leia o valor dos campos de
sbyte
instância.JNIEnv.GetCharField – Leia o valor dos campos de
char
instância.JNIEnv.GetShortField – leia o valor dos campos de
short
instância.JNIEnv.GetIntField – Leia o valor dos campos de
int
instância.JNIEnv.GetLongField – Leia o valor dos campos de
long
instância.JNIEnv.GetFloatField – Leia o valor dos campos de
float
instância.JNIEnv.GetDoubleField – leia o valor dos campos de
double
instância.
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. OIntPtr
valor pode ser uma referência local de JNI, referência global JNI, referência global fraca de JNI ouIntPtr.Zero
(paranull
).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:
JNIEnv.GetStaticObjectField – leia o valor de qualquer campo estático que não seja um tipo interno, como
java.lang.Object
, matrizes e tipos de interface. O valor retornado é uma referência local de JNI.JNIEnv.GetStaticBooleanField – Leia o valor dos
bool
campos estáticos.JNIEnv.GetStaticByteField – Leia o valor dos
sbyte
campos estáticos.JNIEnv.GetStaticCharField – leia o valor dos
char
campos estáticos.JNIEnv.GetStaticShortField – leia o valor dos
short
campos estáticos.JNIEnv.GetStaticLongField – Leia o valor dos
long
campos estáticos.JNIEnv.GetStaticFloatField – leia o valor dos
float
campos estáticos.JNIEnv.GetStaticDoubleField – Leia o valor de
double
campos estáticos.
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:
JNIEnv.SetStaticField) – Escreva o valor de qualquer campo estático que não seja um tipo interno, como
java.lang.Object
, matrizes e tipos de interface. OIntPtr
valor pode ser uma referência local de JNI, referência global JNI, referência global fraca de JNI ouIntPtr.Zero
(paranull
).JNIEnv.SetStaticField) – Escreva o valor de
bool
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
sbyte
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
char
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
short
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
int
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
long
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
float
campos estáticos.JNIEnv.SetStaticField) – Escreva o valor de
double
campos estáticos.
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 .
JNIEnv.CallObjectMethod – invoque um método que retorna um tipo não interno, como
java.lang.Object
, matrizes e interfaces. O valor retornado é uma referência local de JNI.JNIEnv.CallBooleanMethod – invoque um método que retorna um
bool
valor.JNIEnv.CallByteMethod – invoque um método que retorna um
sbyte
valor.JNIEnv.CallCharMethod – invoque um método que retorna um
char
valor.JNIEnv.CallShortMethod – invoque um método que retorna um
short
valor.JNIEnv.CallLongMethod – invoque um método que retorna um
long
valor.JNIEnv.CallFloatMethod – invoque um método que retorna um
float
valor.JNIEnv.CallDoubleMethod – invoque um método que retorna um
double
valor.
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.
JNIEnv.CallNonvirtualObjectMethod – não virtualmente invoca um método que retorna um tipo não interno, como
java.lang.Object
, matrizes e interfaces. O valor retornado é uma referência local de JNI.JNIEnv.CallNonvirtualBooleanMethod – não virtualmente invoca um método que retorna um
bool
valor.JNIEnv.CallNonvirtualByteMethod – não virtualmente invoca um método que retorna um
sbyte
valor.JNIEnv.CallNonvirtualCharMethod – não virtualmente invoca um método que retorna um
char
valor.JNIEnv.CallNonvirtualShortMethod – não virtualmente invoca um método que retorna um
short
valor.JNIEnv.CallNonvirtualLongMethod – não virtualmente invoca um método que retorna um
long
valor.JNIEnv.CallNonvirtualFloatMethod – não virtualmente invoca um método que retorna um
float
valor.JNIEnv.CallNonvirtualDoubleMethod – não virtualmente invoca um método que retorna um
double
valor.
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 .
JNIEnv.CallStaticObjectMethod – invoque um método estático que retorna um tipo não interno, como
java.lang.Object
, matrizes e interfaces. O valor retornado é uma referência local de JNI.JNIEnv.CallStaticBooleanMethod – invoque um método estático que retorna um
bool
valor.JNIEnv.CallStaticByteMethod – invoque um método estático que retorna um
sbyte
valor.JNIEnv.CallStaticCharMethod – invoque um método estático que retorna um
char
valor.JNIEnv.CallStaticShortMethod – invoque um método estático que retorna um
short
valor.JNIEnv.CallStaticLongMethod – invoque um método estático que retorna um
long
valor.JNIEnv.CallStaticFloatMethod – invoque um método estático que retorna um
float
valor.JNIEnv.CallStaticDoubleMethod – invoque um método estático que retorna um
double
valor.
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"
parasbyte
."S"
parashort
."I"
paraint
."J"
paralong
."F"
parafloat
."D"
paradouble
."C"
parachar
."Z"
parabool
."V"
paravoid
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:
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'$'
.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$State
de 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.Class
ambos 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
, 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);
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.