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

이는 콜백에 가장 쉬운 경로 이기 때문에 가장 간단한 형태로 콜백을 작동 하려는 경우 abstract를 사용 하는 것이 좋습니다.This 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.

Java를 구현할 C# 클래스부터 살펴보겠습니다.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:

  • Java에서 유용한 패키지 이름을 생성 하는 [Register] .이를 제외 하 고 자동으로 생성 된 패키지 이름을 가져옵니다.[Register] generates a nice package name in Java--you will get an auto-generated package name without it.
  • Xamarin을 통해 클래스를 실행 하기 위해 .NET 포함에 Java.Lang.Object 신호를 서브클래싱 합니다.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은 Java 개체와 동등한 C#기능을 만드는 데 사용 됩니다.(IntPtr, JniHandleOwnership) constructor: is what Xamarin.Android will use for creating the C#-equivalent of Java objects.
  • [Export]은 메서드를 Java에 노출 하도록 Xamarin.ios에 신호를 보냅니다.[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();
    }
}

JavaCallbacksJava.Lang.Object인 한이를 테스트 하는 모든 클래스가 될 수 있습니다.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에서 AbstractClass 구현 됨Implemented 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#
  • 생성자가 현재로 C# 표시 되어 있기 때문에 throws Throwable추가 되었습니다 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. Java에 대 한 호출을 전달 C# 하는 AbstractClass의 서브 클래스입니다.This is a subclass of AbstractClass that forwards C# calls to Java. Java 개체가 C# 전 세계에 있고 동일한 C# 형식이 abstract 인 경우 xamarin.ios는 코드 내에서 C# C# 사용 하기 위해Invoker접미사가 포함 된 형식을 자동으로 찾습니다.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.

Xamarin.ios는 Java 바인딩 프로젝트에서이 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
  • C# 클래스를 서브클래싱하는 Java 클래스에 대 한 JNI 참조를 보유 하는 class_ref 추가 되었습니다.Added class_ref to hold the JNI reference to the Java class that subclasses our C# class
  • JNI 참조를 포함 하는 id_gettext를 Java getText 메서드에 추가 했습니다.Added id_gettext to hold the JNI reference to the Java getText method
  • (IntPtr, JniHandleOwnership) 생성자를 포함 합니다.Included a (IntPtr, JniHandleOwnership) constructor
  • Invoker에 대 한 세부 정보를 확인 하기 위해 Xamarin.ios에 대 한 요구 사항으로 ThresholdTypeThresholdClass 구현 됨Implemented ThresholdType and ThresholdClass as a requirement for Xamarin.Android to know details about the Invoker
  • GetText 적절 한 JNI 서명으로 Java getText 메서드를 조회 하 고 호출 하는 데 필요 합니다.GetText needed to lookup the Java getText method with the appropriate JNI signature and call it
  • Dispose은에 대 한 참조를 지우는 데만 필요 class_refDispose 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 interop에 대 한 자세한 내용은이 주제에 대 한 놀라운 Xamarin Android 설명서 를 참조 하세요.For details on Java interop, see the amazing Xamarin.Android documentation on this subject.

인터페이스Interfaces

인터페이스는 다음과 같은 한 가지 세부 사항을 제외 하 고 추상 클래스와 거의 같습니다. Xamarin.ios는 이러한 항목에 대 한 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);
}

이는 Xamarin.ios 인터페이스 이지만 .NET 포함에 IJavaObject 신호를 전달 하는 것이 고, 그렇지 않은 경우 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.ios는 현재이 인터페이스에 대 한 Java 코드를 생성 하지 않으므로 C# 프로젝트에 다음 java를 추가 합니다.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);
}

어디에 나 파일을 저장할 수 있지만, 빌드 작업을 AndroidJavaSource으로 설정 해야 합니다.You can place the file anywhere, but make sure to set its build action to AndroidJavaSource. 그러면 AAR 파일로 컴파일하기 위해 적절 한 디렉터리에 복사 하는 .NET 포함을 알립니다.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.ios는 Invoker를 조회 하지 않습니다 .If you followed the abstract class example above, it would work except for one detail: Xamarin.Android won't lookup the Invoker.

이 문제를 해결 하려면abstractC# 되도록 클래스를 수정 합니다.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.ios는 VirtualClassInvoker를 선택 하 고 Java는 메서드에서 @Override를 사용할 수 있습니다.Xamarin.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 Android 지원 인터페이스에서 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.ios에서 가상 클래스에 대 한 Invoker를 로드 하는 방법을 만듭니다.Make 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 포함에 대 한 Invoker 클래스 생성Generate Invoker classes for .NET Embedding automatically
    • 이는 복잡 하지만 심지어.This is going to be complicated, but doable. Xamarin.ios는 Java 바인딩 프로젝트에 대해 이미 이와 유사한 작업을 수행 하 고 있습니다.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