在Xamarin.Android中,C#通过绑定调用JAVA类库,一种抽离JAVA原生接口的底层细节机制。Xamarin.Android提供了一个用于生成绑定的工具,此工具允许开发人员控制如何使用元数据创建绑定,比如改变程序的命名空间,成员重命名等。本文主要讨论元数据如何运作,总结元数据支持的属性以及阐述更改元数据解决绑定出现的问题。
概述
Xamarin.Android通过绑定生成器,自动完成一些绑定中的问题。当绑定JAVA类库时,Xamarin.Android将会自动检测JAVA类库中的所有类,并生成要绑定的所有包、所有类以及成员的列表。生成的APIs列表一般写在项目XML中,在Debug模式下,保存路径为project \obj\Debug\api.xml;在Release模式下,保存路径为project\obj\Release\api.xml。
绑定生成器将使用api.xml文件作为生成必要的C #包装类指南,此XML文件的内容其实是谷歌Android开放源码的变体。下面的内容是摘自api.xml中的部分内容:
在以上的例子中,api.xml在android包中声明了一个继承java.lang.Object的类Manifest。
但是,在许多情况下,还是需要手动修改Metadata.xml文件,以便让绑定通过编译或者让JAVA更符合c#语法。比如,有必要更改JAVA包名,重命名一个类或者改变方法返回类型。
然而,这些更改不是直接通过修改api.xml来实现。相反,这些改变一般都被记录在JAVA绑定类库特殊的xml文件中。当编译Xamarin.Android绑定程序集时,绑定生成器在创建绑定程序集时会受这些映射文件的影响。
相关的XML映射文件一般保存在项目的Transforms文件中。
MetaData.xml –允许修改最终API文件内容,比如修改已创建好的绑定的名字。
EnumFields.xml—包含JAVA整型常量与C#枚举类型之间的映射。
EnumMethods.xml –允许更改JAVA整型常量与C#枚举类型之间的映射方法的参数及返回类型。
其中,MetaData.xml相对比较重要,因为其允许更改一些编译出现的普遍问题:
⦁ 根据.NET语法,重命名类、方法以及属性等;
⦁ 删除无关的类、方法或者属性等;
⦁ 添加额外的支持类,使绑定符合.NET框架模式的设计。
MetaData.xml转换文件
通过上面的介绍,我们知道,绑定生成器通过MetaData.xml文件影响绑定组件的创建。MetaData.XML是一个强大基于XPath语法的工具,可以修改、增加、隐藏及移除API文件中任务的元素。MetaData.XML中所有元素都包含一个路径属性,以便标识要应用该元素的节点。规则顺序如下所示:
- add-node—根据路径属性指定的节点,添加子节点;
- attr—设置由路径属性指定的元素的属性值;
- remove-node—删除匹配的XPath节点。
下面是MetaData.XML的一个例子:
<metadata>
<!-- Normalize the namespace for .NET -->
<attr path="/api/package[@name='com.evernote.android.job']"
name="managedName">Evernote.AndroidJob</attr>
<!-- Don't need these packages for the Xamarin binding/public API -->
<remove-node path="/api/package[@name='com.evernote.android.job.v14']" />
<remove-node path="/api/package[@name='com.evernote.android.job.v21']" />
<!-- Change a parameter name from the generic p0 to a more meaningful one. -->
<attr path="/api/package[@name='com.evernote.android.job']/class[@name='JobManager'] /method[@name='forceApi']/parameter[@name='p0']"
name="name">api</attr>
</metadata>
定位JAVA接口:/interface[@name='AuthListener']
定位JAVA类:/class[@name='MapView']
定位JAVA类或者接口的相关方法:/class[@name='MapView']/method[@name='setTitleSource']
添加类型
add-node元素将向Xamarin.Android项目中的api.XML文件添加一个新的包装类。下面例子用于绑定生成器生成一个包含构造函数及单个字段的类。
<add-node path="/api/package[@name='org.alljoyn.bus']">
<class abstract="false" deprecated="not deprecated" final="false" name="AuthListener.AuthRequest" static="true" visibility="public" extends="java.lang.Object">
<constructor deprecated="not deprecated" final="false" name="AuthListener.AuthRequest" static="false" type="org.alljoyn.bus.AuthListener.AuthRequest" visibility="public" />
<field name="p0" type="org.alljoyn.bus.AuthListener.Credentials" />
</class>
</add-node>
移除类型
指导Xamarin.Android绑定生成器忽略JAVA相关的类型且不绑定。可以在MetaData.XML添加remove-node,代码如下所示:
<remove-node path="/api/package[@name='{package_name}']/class[@name='{name}']" />
成员重命名
直接编辑api.xml文件无法成员重命名,因为Xamarin.Android需要原JAVA原生接口的名称,所以//class/@name无法修改属性。如果真的这样写,创建的绑定将无效。
假设,我们想更改android.Manifest的类型,也许我们会直接编辑api.xml文件,如下所示:
<attr path="/api/package[@name='android']/class[@name='Manifest']"
name="name">NewName</attr>
这将导致绑定生成器创建如下的C#代码:
[Register ("android/NewName")]
public class NewName : Java.Lang.Object { ... }
如上所示,包装类名称已经为NewName,但是原Java类型依然为Manifest。对于Xamarin.Android,包装类绑定了一个不存在的Java类型,因此将无法访问Manifest的任何方法。正确地改变包装类型(方法)的托管名称,如下例所示使用managedName :
<attr path="/api/package[@name='android']/class[@name='Manifest']"
name="managedName">NewName</attr>
包装类事件参数重命名
当Xamarin.Android绑定生成器标识一个监听类型的onXXX方法,C#将生成一个子类即.NET API派生于Java监听基类。下面给出了一个Java类及其方法:
com.someapp.android.mpa.guidance.NavigationManager.on2DSignNextManuever(NextManueverListener listener);
Xamarin.Android将会去除以上方法的前缀on,使用2DSignNextManuever为EventArgs子类名称的基础,如下所示:
NavigationManager.2DSignNextManueverEventArgs
以上并不是一个符合C#语法的类,因此我们需要使用属性argsType并为EventArgs子类取一个符合C#语法的名字。
attr path="/api/package[@name='com.someapp.android.mpa.guidance']/
interface[@name='NavigationManager.Listener']/
method[@name='on2DSignNextManeuver']" name="argsType">NavigationManager.TwoDSignNextManueverEventArgs</attr>
支持的属性
managedName
用于更改包、类、方法或参数的名称,例如改变Java类MyClass的名字为Newclassname:
<attr path="/api/package[@name='com.my.application']/class[@name='MyClass']"
name="managedName">NewClassName</attr>
下例为将java.lang.object.toString方法重命名为Java.Lang.Object.NewManagedName的XPath表达式:
<attr path="/api/package[@name='java.lang']/class[@name='Object'] /method[@name='toString']"
name="managedName">NewMethodName</attr>
managedType
managedType用于更改方法的返回类型。在某些情况下,绑定生成器将无法正确地推断出一个Java方法的返回类型,将会造成编译时错误。这种情况下,可行的解决方案为改变方法的返回类型。
例如,绑定生成器认为 Java方法de.neom.neoreadersdk.resolution.compareTo()应该返回一个int类型,这样将会造成编译错误“Error CS0535: DE.Neom.Neoreadersdk.Resolution' does not implement interface member `Java.Lang.IComparable.CompareTo(Java.Lang.Object)”。下面的代码演示如何将生成的C #方法的返回类型从int更改为java.lang.Object。
<attr path="/api/package[@name='de.neom.neoreadersdk']/class[@name='Resolution'] /method[@name='compareTo'
and count(parameter)=1
and parameter[1][@type='de.neom.neoreadersdk.Resolution']]/parameter[1]"
name="managedType">Java.Lang.Object</attr>
managedReturn
用于更改方法的返回类型,但并不是更改返回属性(返回属性可以导致JNI签名不相容的变化)。注:C#不支持协变返回类型。下面是方法名为append ,将SpannableStringBuilder 改成 IAppendable。
<attr path="/api/package[@name='android.text']/
class[@name='SpannableStringBuilder']/
method[@name='append']"
name="managedReturn">Java.Lang.IAppendable</attr>
obfuscated
Java一些易混淆的类库可能会干扰Xamarin.Android绑定生成器生成C#包装类。易混淆类的特点如下:
类名包好美元符$,例如a$.class;
类名全部为小写字符,例如abc.class。
下面的例子为产生非模糊的C#类:
<attr path="/api/package[@name='{package_name}']/class[@name='{name}']"
name="obfuscated">false</attr>
propertyName
此属性可用于更改托管属性的名称。propertyName使用的特殊情况为Java类的属性只有一个getter方法,绑定生成器将会生成一个.NET不支持的只写属性。下例为将propertyName设置为空字符串,从而移除.NET属性。
<attr path="/api/package[@name='org.java_websocket.handshake'] /class[@name='HandshakeImpl1Client']/method[@name='setResourceDescriptor'
and count(parameter)=1
and parameter[1][@type='java.lang.String']]"
name="propertyName"></attr>
<attr path="/api/package[@name='org.java_websocket.handshake'] /class[@name='HandshakeImpl1Client']/method[@name='getResourceDescriptor'
and count(parameter)=0]"
name="propertyName"></attr>
注意:setter和getter方法仍然会被绑定生成器创建。
sender
指定当方法映射到事件时,方法的参数应为发送方参数。例如:
<attr path="/api/package[@name='android.app']/
interface[@name='TimePickerDialog.OnTimeSetListener']/
method[@name='onTimeSet']/
parameter[@name='view']"
name="sender">true</ attr>
visibility
此属性用于更改类、方法或属性的可见性。例如,Java方法的访问级别为protected,相应的C #包装则为public。
<!-- Change the visibility of a class -->
<attr path="/api/package[@name='namespace']/class[@name='ClassName']" name="visibility">public</attr>
<!-- Change the visibility of a method -->
<attr path="/api/package[@name='namespace']/class[@name='ClassName'] /method[@name='MethodName']" name="visibility">public</attr>
EnumFields.XML and EnumMethods.XML
有些情况下,Android库使用整型常量来表示传递给库相关属性或方法的状态。在许多情况下,C#中的枚举就是和整型常量一一对应。为了有效使用这一映射,可以在绑定的项目文件使用enumfields.xml和enummethods.xml。
EnumFields.XML定义枚举,一般是Java整型常量和C#枚举之间的映射。如下所示:
<mapping jni-class="com/skobbler/ngx/map/realreach/SKRealReachSettings" clr-enum- type="Skobbler.Ngx.Map.RealReach.SKMeasurementUnit">
<field jni-name="UNIT_SECOND" clr-name="Second" value="0" />
<field jni-name="UNIT_METER" clr-name="Meter" value="1" />
<field jni-name="UNIT_MILIWATT_HOURS" clr-name="MilliwattHour" value="2" />
</mapping>
以上的例子,给定了一个Java类SKRealReachSettings ,并在C#命名空间为 Skobbler.Ngx.Map.RealReach下定义了一个枚举SKRealReachSettings。field 实体中定义了Java常量(例如UNIT_SECOND)和C#枚举实体名(Second),以及两个实体的值。
EnumMethods.XML定义Getter/Setter方法,允许改变Java整型常量和C#枚举之间的映射方法的参数及返回类型。
<mapping jni-class="com/skobbler/ngx/map/realreach/SKRealReachSettings">
<method jni-name="getMeasurementUnit" parameter="return" clr-enum-type="Skobbler.Ngx.Map.RealReach.SKMeasurementUnit" />
<method jni-name="setMeasurementUnit" parameter="measurementUnit" clr-enum-type="Skobbler.Ngx.Map.RealReach.SKMeasurementUnit" />
</mapping>
总结
本文主要讨论的是Xamarin.Android使用元数据解决编译问题。希望大家能够给点改进意见。