介绍如何调试Xamarin.Android的binding项目

背景

Xamarin的开发的一个无法避免的弊端就是在很多Android原生态应用中被普遍用到的库,未必有.NET的实现版本。这个问题就如同当时微软WinPhone失败的原因一样,在另外两个平台中非常普遍的应用,在WinPhone中没有或者开发进度缓慢。

Xamarin为了解决这个问题,在Android和iOS的平台下都设计了一个叫做binding library的项目类型,这个项目类型就是为了将user找到的任何已经发布成类库的jar包等,或者iOS下的objective C的类库包转换成一个DLL文件。

本文主要讲述的是Android端的关于jar包的binding。Android还有一种aar包,这种包是带有resource内容的jar包,所以通常理解上来说原理应该是一样的。

内容

本文我主要分两部分,第一部分是通过一个微软资深员工写的troubleshooting的文档来做一个整理归纳,以便用于以后的参考。第二部分是我自己实际做过的troubleshooting中我写过的一些东西,以及对照第一部分中知识点中的原理。


Xamarin.Android binding的troubleshooting技巧与原理

1. 调查 (准备工作)

在着手开始解决这类问题之前,我们首先要安装和准备一些工具,以下是一些有用的工具:

当准备好这些工具后,我们做以下步骤来预调查一下这个问题。

  1. Build发生问题的binding project
  2. Build完会得到full的Diagnostic Build log
  3. 根据Diagnostic Build log来检查错误

这个时候我们可以暂时先不看非常具体的错误信息等,我们要做的是先看一下出错的library的一些基本信息。这个是因为很多情况下,我们binding会build成功,但是其实会缺少类型或者接口等,导致该DLL并无法被正常使用。我遇到过的最极端的情况是,build成功但是没有类生成。

首先,我们要decompiler这个Android的类库。

  • 如果是一个.jar文件,直接拖曳或者在Java Decompiler中打开
  • 如果是一个.aar文件,先extract/unzip这个文件,并且找到classes.jar文件,再在Java Decompiler中打开

得到反编译信息之后,我们去检查这个类库本身的时候,可以从以下几个方面着手,并且看这些方面与我们Diagnostic output中得到的信息有没有关系。

  • 是否有任何具有被混淆(obfuscation)特征的类?(只有小写字母/数字/$)例:a.class / a$.class
  • 是否有带有import语句的类库并没有被我们的binding项目引用?
  • Binding SDK使用到的依赖项(dependencies)的版本是什么?
  • 这个.jar/.aar所支持的Android API level是多少?
  • 这个.jar/.aar是由什么版本的Java SDK编译的?

2. 修复问题

选择适合的AndroidClassParser

Xamarin一共有两种AndroidClassParser可供binding项目使用:

  1. jar2xml 使用Java reflection来从一个.jar文件中提取类型和成员
  2. class-parse 直接解析Java字节码

设置方法:可以在csproj文件中设置相应需要使用的AndroidClassParser

i.e.

  • <AndroidClassParser>class-parse</AndroidClassParser> - 开启Class Parse
  • <AndroidClassParser>jar2xml</AndroidClassParser> - 开启jar2xml

注意:默认的方式是jar2xml。但是,根据我的测试,你无法直接从项目属性中去设置,因为一个默认的项目是用jar2xml,但是项目属性中反而会写是class-parse,如图:

我的做法是使用Notepad++打开.csproj文件,并且在项目的PropertyGroup中添加上述的设置命令。如图:

官方介绍文档:https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-process#binding-project-build-properties

调查api.xml文件

该文件在你第一次build完binding项目就会产生,通常的路径是Binding项目的obj\Debug目录下。这个文件反应了Xamarin binding project是怎么来解析这个jar包的,从这个当中我们可以很大程度上分析出,现在生成的DLL已经有了什么,并且缺少什么。同样,即便是已经成功解析的类型,也可以给我们一个参考,关于其他丢失的类型可以怎么改啊,怎么添加啊等等。

下面是一些常见情况的分析。

缺少引用(Missing Reference)

这个问题主要就是针对上述所说的一种情况,在这个需要绑定的jar包中,引用了其他的类库作为依赖项,并且我们手中只有这个jar包,没有填写其依赖项的情况。

  • 如果缺少的引用是一个NuGet的类库,直接在Binding项目中添加这个NuGet的包即可。例如Android的那些Support的libraries等等。
  • 如果引用的是其他的第三方jar包或者aar包,将他们添加到binding项目的Jars文件夹下,并且设置相应的文件属性类型为ReferenceJar, EmbeddedReferenceJar或者LibraryProjectZip

Java library is required

如果收到错误代码at least one Java library is required即使你添加了一个jar包。

可能的原因

这个问题很可能是因为添加了jar包后直接build,忘记设置该jar包的build action,binding generator不会去猜应该使用EmbeddedJar还是用其他方式,所以需要手动设定。

Java Version Mismatch (Java版本不匹配)

有时候类型压根没有生成,或者发生unexpected的crash的时候,可能是由于我们在Xamarin.Android binding项目中使用的Java SDK版本要比当时编译这个jar包的版本要更新或者更低。我们需要确保的是Java SDK的版本至少兼容,比如说,这个jar包是用Java SDK 8的161版本编译的,那我们使用Java SDK 8的171是可以的我认为,但是如果你使用的是Java SDK 7或者9,那就不行了。

认识和修改Metadata.xml文件方法 (重中之重)

接下去是我们重中之重的话题。之所以我们会需要这篇文档,是因为在转换jar包到DLL的过程中,由于Java语言和C#语言本身的不同性,加上写jar包的人可能也只是一个带有自己编码习惯的程序员,我们在解析jar包的过程中会需要改变一些解析出来的属性。

如同我们刚才说的,api.xml是我们修改Metadata.xml文件的依据,因为它告诉我们目前binding项目是怎么解析这个jar包的。每当我们增加一条设置语句在Metadata.xml文件中,build之后,api.xml文件会发生相应的变化。

首先介绍一下修改Metadata.xml文件我们必须要知道的知识。

常用路径(Common Path)

这个路径表示我们如何定位到api.xml中的某个类型,接口,方法,甚至方法的参数。要知道,我们是可以任意修改当前binding项目解析这个jar包的结果的。

  • /interface EX: /interface[@name='AuthListener']
  • /class EX: /class[@name='MapView']
  • /method EX: /method[@name='setTileSource']
  • /method(with parameters) EX: /method[@name='OnCreate' and count(parameter)=2 and parameter[1][@type='com.my.CustomActivity'] and parameter[2][@type='android.os.Bundle']]
  • /parameter EX: /parameter[@name='p0']
  • /parameter(with type) EX: /parameter[1][@type='com.my.CustomActivity']

常用名称(Common name)

这个是和上面的常用路径一起使用的,当使用路径定位到api.xml中的元素之后,你要修改的任何属性值就是用接下来的这些来定义并且赋值。

  • name="managedType" EX: Java.Lang.Object
  • name="obfuscated" - Changes the obfuscation EX: true / false
  • name="managedName" - Changes the managed name EX: MyCSharpName
  • name="propertyName" - Changes the property name EX: MyPropertyName
  • name="managedReturn" - Changes the managed return type EX: Java.Lang.Object
  • name="argsType" - changes the argument type EX: MyCustomErrorEventArgs
  • name="sender" - Changes which parameter of a method should be the sender parameter when it’s mapped to an event EX: true / false
  • name="eventName" - Changes the event name EX: MyEventName

丢失类型 / 混淆类型 (Missing Types / Obfuscated Types)

当我们看到jar包或者aar包中有混淆类型的时候,我们必须要unobfuscate,这样我们的binding generator才可以正确生成相应的C#类型。

语法:

1
<attr path="api/package[@name='{package_name}']/class[@name='{name}']" name="obfuscated">false</attr>

重复命名或标准化命名(Duplicate Names or Normalizing Names)

有时会碰到duplicate的managedName或者原本的jar包中的命名不符合你的编程习惯,你可以使用此attribute来修改。

注意只有前一种情况会影响我们生成DLL,这个情况会报错,错误类似于如下截图:

如果你点击进入这个错误,你可能会发现后台生成的类大概会变成如下的样子:

解决方法:

1
<attr path="/api/package[@name='{package_name}']/class[@name='{name}']" name="managedName">NewManagedName</attr>

类可见性(Class Visibility)

Binding Generator不会生成non-public的类或者派生类。通常情况下只要将类的可见性变成public就可以修复这个问题。

语法:

1
<attr path="/api/package[@name='{package_name}']/class[@name='{name}']" name="visibility">public</attr>

重复的自定义EventArgs类型 (Duplicate custom EventArgs types)

这个类型的问题会导致build错误。你会看到类似如下的错误信息:

1
`error CS0102: The type `Com.Google.Ads.Mediation.DismissScreenEventArgs' already contains a definition for `p0'`
可能的原因

这个的最大可能的原因是有一些Interface会有一些共同的事件(Event),并且很可能这些事件的命名是一样的。在Java中会自动帮忙处理这些命名,但是binding generator不会。

假设现在我们有以下两个Java的Interface,分别为MediationBannerListener以及MediationInserstitialListener,他们各自拥有自己的onDismissScreen方法并且方法参数命名为p0,这时binding generator会给这两个接口同时创建DismissScreenEventArgs,这样就会最终导致错误。

1
2
3
4
5
6
7
public interface MediationBannerListener{
void onDismissScreen(MediationBannerAdapter p0);
}

public interface MediationInterstitialListener{
void onDismissScreen(MediationInterstitialAdapter p0);
}
解决方法

这个本身是一个by design的情况,Java会avoid掉长命名之类的。为了解决这个问题,我们还是修改Metadata.xml,如下:

1
2
3
4
5
6
7
<attr path="api/package[@name='com.google.ads.mediation']/
interface[@name='MediationBannerListener']/method[@name='onDismissScreen']"
name="argsType">BannerDismissScreenEventArgs</attr>

<attr path="api/package[@name='com.google.ads.mediation']/
interface[@name='MediationInserstitialListener']/method[@name='onDismissScreen']"
name="argsType">IntersitionalDismissScreenEventArgs</attr>

类没有实现接口中的方法(Class does not implement interface method)

这个在C#中的意思就是有一个接口,里面比如定义了一些方法,有个类继承这个接口,那它就要实现所有这个接口中的方法,如果漏掉了,那么编译器会报这个错。
但是在Binding的项目中,往往我们会在api.xml中的该类中找到这个方法,表明确实是实现了。这个时候也许你看到的error会是如下的error:

1
obj\Debug\generated\src\Oauth.Signpost.Basic.HttpURLConnectionRequestAdapter.cs(8,23): error CS0738: 'Oauth.Signpost.Basic.HttpURLConnectionRequestAdapter' does not implement interface member 'Oauth.Signpost.Http.IHttpRequest.Unwrap()'. 'Oauth.Signpost.Basic.HttpURLConnectionRequestAdapter.Unwrap()' cannot implement 'Oauth.Signpost.Http.IHttpRequest.Unwrap()' because it does not have the matching return type of 'Java.Lang.Object'
可能的原因

这个问题的原因其实是因为使用协变(covariant)返回类型来绑定Java方法。在上述的错误代码中,方法Oauth.Signpost.Http.IHttpRequest.UnWrap()需要返回的是Java.Lang.Object。但是我们的binding generator其实是认为它返回的是HttpURLConnection

解决方法
  • 增加一个部分类(partial class)的声明,这个类为HttpURLConnectionRequestAdapter,并且显示地实现IHttpRequest.Unwrap():

    1
    2
    3
    4
    5
    6
    7
    namespace Oauth.Signpost.Basic{
    partial class HttpURLConnectionRequestAdapter{
    Java.Lang.Object OauthSignpost.Http.IHttpRequest.Unwrap(){
    return Unwrap();
    }
    }
    }

    这个方法的原理其实就是你原本的binding generator怎么生成这个方法,我不管,我手动提供给你一个这样的方法,因为是部分类,所以会build的时候被合并,相当于给这个方法多一个签名。

  • 去除这个方法的协变性,也就是说,通过改Metadata.xml文件去将这个方法的return类型给改掉。

    1
    2
    3
    4
    <attr
    path="api/package[@name='oauth.signpost.basic']/class[@name='HttpURLConnectionRequestAdapter']
    /method[@name='unwrap']" name="managedReturn">Java.Lang.Object
    </attr>

添加类型(Adding Types)

可以使用<add-node>来添加任何东西在api.xml文件中。不过通常来说只会添加类,改变构造函数,或者更改一个泛型类型。

以下例子是创建一个类,并且该类具有一个构造函数和字段。

1
2
3
4
5
6
<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>

删除类型(Removing Types)

删除一个类型是非常简单的,通常情况就是你知道你不会使用这个类。但是需要注意的点是在删除之前你必须检查一下其他你需要用到的类中是否有引用这个类的地方,否则可能会出错。

语法:

1
<remove-node path="api/package[@name='{package_name}']/class[@name='{name}']" />

几个成功并且Common的Metadata修改的实例

本来是想使用自己的,但是发现GitHub上的这几个实例更好,因为这些实例中给出了一些常见的问题写在注释中,并且下面根据这个问题怎么处理。

Binding ADTECH Mobile

Binding Brother Print SDK for Android

Binding NeoReaderSDK

Binding Java WebSocket

Binding Socialize Android SDK

使用Java注释(Using Java Annotations)

  1. 确保使用[Export]到相应的方法/类/其他。
  2. 并且确保你引用了Mono.Android.Export到Xamarin.Android项目

Java.Interop.ExportAttribute Class


3. 一些术语

这里我就直接拷贝了那个senior member的内容了,英语版看起来比较专业。

3. Terms

JNI (Java Native Interface)

In computing, the Java Native Interface (JNI) is a programming framework that enables Java code running in a Java Virtual Machine (JVM) to call and be called by native applications (programs specific to a hardware and operating system platform) and libraries written in other languages such as C, C++ and assembly.

Android Callable Wrappers (ACW)

Android callable wrappers are a JNI bridge that are used whenver the Android runtime needs to invoke managed code.

Managed Callable Wrappers (MCW)

Managed callable wrappers are a JNI bridge that are used whenever managed code needs to invoke Android code and provide support for overriding virtual methods and implementing Java interfaces.

Embedded vs. Non-Embedded

When using a Build Action such as EmbeddedJar or EmbeddedReferenceJar, it will embed the respective library into the .apk so it will be available at runtime. Otherwise it is expected that either the Device or the application will provide the .jar at runtime. (I.E. It is already loaded on device or will be provided via a download/etc)

Reference vs. Non-Reference

When using a Build Action such as ReferenceJar or EmbeddedReferenceJar, it will not generate Manage Callable Wrappers(ACW) and will not be exposed to the client.

Java is not the same as C#

Because of this limitation, you will need to be aware of the respective generated C# code as there might be certain things that the languages handle differently.

EX: Java -> C#

  • get/set methods -> properties
  • fields -> properties * listeners -> events
  • static nested class -> nested class
  • inner class -> nested class with an instance constructor

4. Conclusion

Although Xamarin.Android Binding errors might be confusing and the JNI might be intimidating, there is always a few ways to work around the issue at hand. Documentation:

Binding a Jar

Binding a Library Project

Java Bindings Metadata

Mono Metadata

Creating Bindings Using Metadata

Naming Parameters With Javadoc

Troubleshooting

XPath Tutorial

XPath Functions

Xamarin Univeristy Course:

https://university.xamarin.com/classes/track/xamarin-android#and450-binding

如何在Xamarin.Forms项目中使用Bot Service(WebView版) 如何启用滑动手势当Xamarin.Forms中的NavigationBar被设为hidden的时候
You need to set install_url to use ShareThis. Please set it in _config.yml.

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×