Android 上的回调Callbacks on Android

从C#调用到 Java 是一个有风险的业务Calling to Java from C# is somewhat a risky business. 这就是说,从C#到 Java 的回调模式;但是,这比我们喜欢的复杂。That is to say there is a pattern for callbacks from C# to Java; however, it is more complicated than we would like.

我们将介绍三个用于执行最适合 Java 的回调的选项:We'll cover the three options for doing callbacks that make the most sense for Java:

  • 抽象类Abstract classes
  • 接口Interfaces
  • 虚方法Virtual methods

抽象类Abstract Classes

这是最简单的回调路由,因此,如果你只是想要在最简单的形式使用回拨,则建议使用 abstractThis is the easiest route for callbacks, so I would recommend using abstract if you are just trying to get a callback working in the simplest form.

让我们从一个C#类开始,我们希望 Java 实现:Let's start with a C# class we would like Java to implement:

[Register("mono.embeddinator.android.AbstractClass")]
public abstract class AbstractClass : Java.Lang.Object
{
    public AbstractClass() { }

    public AbstractClass(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { }

    [Export("getText")]
    public abstract string GetText();
}

下面是执行此操作的详细信息:Here are the details to make this work:

  • [Register] 在 Java 中生成良好的包名称--你将在不使用它的情况下获取自动生成的包名称。[Register] generates a nice package name in Java--you will get an auto-generated package name without it.
  • 子类化 Java.Lang.Object 向 .NET 嵌入发出的信号通过 Xamarin 的 Java 生成器运行类。Subclassing Java.Lang.Object signals to .NET Embedding to run the class through Xamarin.Android's Java generator.
  • 空构造函数:将从 Java 代码使用。Empty constructor: is what you will want to use from Java code.
  • (IntPtr, JniHandleOwnership) 构造函数: Xamarin Android 将使用它来创建C#等效的 Java 对象。(IntPtr, JniHandleOwnership) constructor: is what Xamarin.Android will use for creating the C#-equivalent of Java objects.
  • [Export] 信号 Xamarin 向 Java 公开方法。[Export] signals Xamarin.Android to expose the method to Java. 由于 Java 世界喜欢使用小写方法,因此还可以更改方法名称。We can also change the method name, since the Java world likes to use lower case methods.

接下来,我们将C#创建一个方法来测试方案:Next let's make a C# method to test the scenario:

[Register("mono.embeddinator.android.JavaCallbacks")]
public class JavaCallbacks : Java.Lang.Object
{
    [Export("abstractCallback")]
    public static string AbstractCallback(AbstractClass callback)
    {
        return callback.GetText();
    }
}

只要 Java.Lang.ObjectJavaCallbacks 就可以是用于测试此情况的任何类。JavaCallbacks could be any class to test this, as long as it is a Java.Lang.Object.

现在,请在 .NET 程序集上运行 .NET 嵌入以生成 AAR。Now, run .NET Embedding on your .NET assembly to generate an AAR. 有关详细信息,请参阅入门指南See the Getting Started guide for details.

将 AAR 文件导入 Android Studio 后,让我们编写单元测试:After importing the AAR file into Android Studio, let's write a unit test:

@Test
public void abstractCallback() throws Throwable {
    AbstractClass callback = new AbstractClass() {
        @Override
        public String getText() {
            return "Java";
        }
    };

    assertEquals("Java", callback.getText());
    assertEquals("Java", JavaCallbacks.abstractCallback(callback));
}

我们:So we:

  • 使用匿名类型在 Java 中实现 AbstractClassImplemented the AbstractClass in Java with an anonymous type
  • 确保实例从 Java 返回 "Java"Made sure our instance returns "Java" from Java
  • 确保实例返回 "Java"C#Made sure our instance returns "Java" from C#
  • 添加了 throws Throwable, C#因为构造函数当前标记有 throwsAdded throws Throwable, since C# constructors are currently marked with throws

如果我们按原样运行此单元测试,则会失败并出现如下错误:If we ran this unit test as-is, it would fail with an error such as:

System.NotSupportedException: Unable to find Invoker for type 'Android.AbstractClass'. Was it linked away?

此处缺少的是 Invoker 类型。What is missing here is an Invoker type. 这是将调用转发C#到 Java 的 AbstractClass 的子类。This is a subclass of AbstractClass that forwards C# calls to Java. 如果C# Java 对象进入世界并且等效C#类型是抽象的,则 Xamarin 会自动查找具有后缀的C#类型Invoker在代码中C#使用。If a Java object enters the C# world and the equivalent C# type is abstract, then Xamarin.Android automatically looks for a C# type with the suffix Invoker for use within C# code.

对于 Java 绑定项目,Xamarin 使用这种 Invoker 模式。Xamarin.Android uses this Invoker pattern for Java binding projects among other things.

下面是 AbstractClassInvoker的实现:Here is our implementation of AbstractClassInvoker:

class AbstractClassInvoker : AbstractClass
{
    IntPtr class_ref, id_gettext;

    public AbstractClassInvoker(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer)
    {
        IntPtr lref = JNIEnv.GetObjectClass(Handle);
        class_ref = JNIEnv.NewGlobalRef(lref);
        JNIEnv.DeleteLocalRef(lref);
    }

    protected override Type ThresholdType
    {
        get { return typeof(AbstractClassInvoker); }
    }

    protected override IntPtr ThresholdClass
    {
        get { return class_ref; }
    }

    public override string GetText()
    {
        if (id_gettext == IntPtr.Zero)
            id_gettext = JNIEnv.GetMethodID(class_ref, "getText", "()Ljava/lang/String;");
        IntPtr lref = JNIEnv.CallObjectMethod(Handle, id_gettext);
        return GetObject<Java.Lang.String>(lref, JniHandleOwnership.TransferLocalRef)?.ToString();
    }

    protected override void Dispose(bool disposing)
    {
        if (class_ref != IntPtr.Zero)
            JNIEnv.DeleteGlobalRef(class_ref);
        class_ref = IntPtr.Zero;

        base.Dispose(disposing);
    }
}

这里有相当多的一点,我们:There is quite a bit going on here, we:

  • 添加了子类 Invoker 的类 AbstractClassAdded a class with the suffix Invoker that subclasses AbstractClass
  • 添加了 class_ref 以保存对C#类的子类的 Java 类的 JNI 引用Added class_ref to hold the JNI reference to the Java class that subclasses our C# class
  • 添加了 id_gettext 以保存对 Java getText 方法的 JNI 引用Added id_gettext to hold the JNI reference to the Java getText method
  • 包含 (IntPtr, JniHandleOwnership) 构造函数Included a (IntPtr, JniHandleOwnership) constructor
  • 已实现 ThresholdTypeThresholdClass,以要求 Xamarin 了解有关 Invoker 的详细信息Implemented ThresholdType and ThresholdClass as a requirement for Xamarin.Android to know details about the Invoker
  • GetText 需要在 Java getText 方法中查找适当的 JNI 签名并将其调用GetText needed to lookup the Java getText method with the appropriate JNI signature and call it
  • 只需清除对 class_ref 的引用 DisposeDispose is just needed to clear the reference to class_ref

添加此类并生成新的 AAR 后,我们的单元测试通过。After adding this class and generating a new AAR, our unit test passes. 如您所见,这种回调模式并不理想,但可行。As you can see this pattern for callbacks is not ideal, but doable.

有关 Java 互操作的详细信息,请参阅本主题中的精彩Xamarin 文档For details on Java interop, see the amazing Xamarin.Android documentation on this subject.

接口Interfaces

接口与抽象类几乎相同,但有一种细节:Xamarin 不会为它们生成 Java。Interfaces are much the same as abstract classes, except for one detail: Xamarin.Android does not generate Java for them. 这是因为在 .NET 嵌入之前,Java 将实现一个C#接口。This is because before .NET Embedding, there are not many scenarios where Java would implement a C# interface.

假设有以下C#接口:Let's say we have the following C# interface:

[Register("mono.embeddinator.android.IJavaCallback")]
public interface IJavaCallback : IJavaObject
{
    [Export("send")]
    void Send(string text);
}

IJavaObject 信号发送到 .NET,这是一个 Xamarin Android 接口,否则这与 abstract 类完全相同。IJavaObject signals to .NET Embedding that this is a Xamarin.Android interface, but otherwise this is exactly the same as an abstract class.

由于 Xamarin 目前不会为此接口生成 Java 代码,因此请将以下 Java 添加到C#项目:Since Xamarin.Android will not currently generate the Java code for this interface, add the following Java to your C# project:

package mono.embeddinator.android;

public interface IJavaCallback {
    void send(String text);
}

你可以在任何位置放置该文件,但请确保将其生成操作设置为 AndroidJavaSourceYou can place the file anywhere, but make sure to set its build action to AndroidJavaSource. 这将向 .NET 嵌入发出信号,以便将其复制到适当的目录以编译到 AAR 文件中。This will signal .NET Embedding to copy it to the proper directory to get compiled into your AAR file.

接下来,Invoker 实现将完全相同:Next, the Invoker implementation will be quite the same:

class IJavaCallbackInvoker : Java.Lang.Object, IJavaCallback
{
    IntPtr class_ref, id_send;

    public IJavaCallbackInvoker(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer)
    {
        IntPtr lref = JNIEnv.GetObjectClass(Handle);
        class_ref = JNIEnv.NewGlobalRef(lref);
        JNIEnv.DeleteLocalRef(lref);
    }

    protected override Type ThresholdType
    {
        get { return typeof(IJavaCallbackInvoker); }
    }

    protected override IntPtr ThresholdClass
    {
        get { return class_ref; }
    }

    public void Send(string text)
    {
        if (id_send == IntPtr.Zero)
            id_send = JNIEnv.GetMethodID(class_ref, "send", "(Ljava/lang/String;)V");
        JNIEnv.CallVoidMethod(Handle, id_send, new JValue(new Java.Lang.String(text)));
    }

    protected override void Dispose(bool disposing)
    {
        if (class_ref != IntPtr.Zero)
            JNIEnv.DeleteGlobalRef(class_ref);
        class_ref = IntPtr.Zero;

        base.Dispose(disposing);
    }
}

生成 AAR 文件后,在 Android Studio 可以编写以下传递单元测试:After generating an AAR file, in Android Studio we could write the following passing unit test:

class ConcreteCallback implements IJavaCallback {
    public String text;
    @Override
    public void send(String text) {
        this.text = text;
    }
}

@Test
public void interfaceCallback() {
    ConcreteCallback callback = new ConcreteCallback();
    JavaCallbacks.interfaceCallback(callback, "Java");
    assertEquals("Java", callback.text);
}

虚方法Virtual Methods

在 Java 中替代 virtual 是可能的,但不是很好的体验。Overriding a virtual in Java is possible, but not a great experience.

假设您具有以下C#类:Let's assume you have the following C# class:

[Register("mono.embeddinator.android.VirtualClass")]
public class VirtualClass : Java.Lang.Object
{
    public VirtualClass() { }

    public VirtualClass(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { }

    [Export("getText")]
    public virtual string GetText() { return "C#"; }
}

如果遵循上面的 abstract 类示例,则除了一种详细信息外,它还可以工作:Xamarin 不查找 InvokerIf you followed the abstract class example above, it would work except for one detail: Xamarin.Android won't lookup the Invoker.

若要解决此问题, C#请修改要abstract的类:To fix this, modify the C# class to be abstract:

public abstract class VirtualClass : Java.Lang.Object

这并不理想,但这种情况可行。This is not ideal, but it gets this scenario working. Xamarin 会选取 VirtualClassInvoker,Java 可以在方法上使用 @OverrideXamarin.Android will pick up the VirtualClassInvoker and Java can use @Override on the method.

将来的回调Callbacks in the Future

我们可以通过几种方式来改进这些方案:There are a couple of things we could to do improve these scenarios:

  1. PR C#上throws Throwable已修复构造函数。throws Throwable on C# constructors is fixed on this PR.
  2. 在 Xamarin 中创建 Java 生成器支持接口。Make the Java generator in Xamarin.Android support interfaces.
    • 这无需添加具有 AndroidJavaSource的生成操作的 Java 源文件。This removes the need for adding Java source file with a build action of AndroidJavaSource.
  3. 为 Xamarin 加载用于虚拟类的 InvokerMake a way for Xamarin.Android to load an Invoker for virtual classes.
    • 这无需在 virtual 示例 abstract中标记该类。This removes the need to mark the class in our virtual example abstract.
  4. 自动生成用于 .NET 嵌入的 InvokerGenerate Invoker classes for .NET Embedding automatically
    • 这会比较复杂,但可行。This is going to be complicated, but doable. 对于 Java 绑定项目,Xamarin 已经执行了类似于此的操作。Xamarin.Android is already doing something similar to this for Java binding projects.

这里有很多工作要做,但这些 .NET 嵌入功能的增强功能是可行的。There is a lot of work to be done here, but these enhancements to .NET Embedding are possible.

其他阅读材料Further Reading