Java 和托管代码互操作性

应用开发人员希望能够使用以其中一种 .NET 托管语言编写的代码来调用本机 Android API,并接收来自 Android API 的调用或响应事件。 .NET for Android 在生成和运行时采用多种方法来桥接 Java VM(Android OS 中的 ART)和托管 VM (MonoVM)。

Java VM 和托管 VM 作为独立实体共存于同一进程或应用中。 尽管共享相同的进程资源,但无法直接从 .NET 调用 Java/Kotlin API,而且 Java/Kotlin 代码也无法直接调用托管代码 API。 为了启用此通信,.NET for Android 使用 Java 原生接口 (JNI)。 此方法支持本机代码(.NET 托管代码在此上下文中为本机代码)注册 Java 方法的实现,这些是方法在 Java VM 外部以 Java 或 Kotlin 之外的语言编写的。 需要在 Java 代码中声明这些方法,例如:

class MainActivity extends androidx.appcompat.app.AppCompatActivity
{
  public void onCreate (android.os.Bundle p0)
  {
    n_onCreate (p0);
  }

  private native void n_onCreate (android.os.Bundle p0);
}

每个本机方法都使用 native 关键字进行声明,每当从 Java 代码调用它时,Java VM 都将使用 JNI 调用目标方法。

本机方法可以动态或静态注册,即提供一个本地共享库,由其导出一个具有适当名称的符号,指向实现 Java 方法的本机地函数。

Java 可调用包装器

.NET for Android 通过生成对 Java/Kotlin API 进行镜像的 C# 代码来包装 Android API。 每个与 Java/Kotlin 类型对应的生成类都派生自 Java.Lang.Object 类(在 Mono.Android 程序集中实现),该类将其标记为 Java 可互操作类型。 这意味着它可以实现或替代虚拟 Java 方法。 要使注册和调用这些方法成为可能,必须生成 Java 类,该类可镜像托管类并向托管转换提供 Java 的入口点。 Java 类是在应用生成期间以及 .NET for Android 生成期间生成的,称为 Java 可调用包装器 (JCW)。 以下示例展示了替代两个 Java 虚拟方法的托管类:

public class MainActivity : AppCompatActivity
{
    public override Android.Views.View? OnCreateView(Android.Views.View? parent, string name, Android.Content.Context context, Android.Util.IAttributeSet attrs)
    {
        return base.OnCreateView(parent, name, context, attrs);
    }

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        DoSomething(savedInstanceState);
    }

    void DoSomething(Bundle bundle)
    {
        // do something with the bundle
    }
}

在本示例中,托管类替代 AppCompatActivity 类型中的 OnCreateViewOnCreateJava 虚拟方法。 DoSomething 方法与基本 Java 类型中的任何方法都不对应,因此它不会包含在 JCW 中。

以下示例展示了为上述类生成的 JCW(为清楚起见,省略了某些生成的方法):

public class MainActivity extends androidx.appcompat.app.AppCompatActivity
{
  public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3)
  {
    return n_onCreateView (p0, p1, p2, p3);
  }
  private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3);

  public void onCreate (android.os.Bundle p0)
  {
    n_onCreate (p0);
  }
  private native void n_onCreate (android.os.Bundle p0);
}

注册

这两种方法注册方式都依赖于生成 JCW,其中的动态注册方式需要生成更多代码,以便可以在运行时执行注册。

仅针对派生自 Java.Lang.Object 类型的类型生成 JCW。 Java.Interop 可读取应用及其库引用的所有程序集,其任务是查找这些类型。 然后,返回的程序集列表由各种任务使用,JCW 就是其中一个。

找到所有类型后,将分析每种类型中的每个方法,以查找那些替代虚拟 Java 方法的方法,因此需要将其包含在包装器类代码中。 生成器还会检查给定方法是否可以静态注册。

生成器会查找使用 Register 特性修饰的方法,最常通过调用其包含 Java 方法名称、JNI 方法签名和连接器方法名称的构造函数来创建这些方法。 连接器是一种静态方法,用于创建委托,随后允许调用本机回调方法:

public class MainActivity : AppCompatActivity
{
    // Connector backing field
    static Delegate? cb_onCreate_Landroid_os_Bundle_;

    // Connector method
    static Delegate GetOnCreate_Landroid_os_Bundle_Handler()
    {
        if (cb_onCreate_Landroid_os_Bundle_ == null)
            cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate((_JniMarshal_PPL_V)n_OnCreate_Landroid_os_Bundle_);
        return cb_onCreate_Landroid_os_Bundle_;
    }

    // Native callback
    static void n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
    {
        var __this = global::Java.Lang.Object.GetObject<Android.App.Activity>(jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
        var savedInstanceState = global::Java.Lang.Object.GetObject<Android.OS.Bundle>(native_savedInstanceState, JniHandleOwnership.DoNotTransfer);
        __this.OnCreate(savedInstanceState);
    }

    // Target method
    [Register("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]
    protected virtual unsafe void OnCreate(Android.OS.Bundle? savedInstanceState)
    {
        const string __id = "onCreate.(Landroid/os/Bundle;)V";
        try
        {
            JniArgumentValue* __args = stackalloc JniArgumentValue[1];
            __args[0] = new JniArgumentValue((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object)savedInstanceState).Handle);
            _members.InstanceMethods.InvokeVirtualVoidMethod(__id, this, __args);
        }
        finally
        {
            global::System.GC.KeepAlive(savedInstanceState);
        }
    }
}

以上代码是在生成 .NET for Android 时在 Android.App.Activity 类中生成的。 此代码的作用取决于注册方法。

动态注册

在调试配置中生成应用或关闭封送方法时,.NET for Android 使用此注册方法。

借助动态注册,可为 Java 可调用包装器中所示的 C# 示例生成以下 Java 代码(为清楚起见,省略了某些生成的方法):

public class MainActivity extends androidx.appcompat.app.AppCompatActivity
{
  public static final String __md_methods;
  static
  {
    __md_methods =
            "n_onCreateView:(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;:GetOnCreateView_Landroid_view_View_Ljava_lang_String_Landroid_content_Context_Landroid_util_AttributeSet_Handler\n" +
            "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" +
            "";
    mono.android.Runtime.register ("HelloAndroid.MainActivity, HelloAndroid", MainActivity.class, __md_methods);
  }

  public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3)
  {
    return n_onCreateView (p0, p1, p2, p3);
  }

  private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3);

  public void onCreate (android.os.Bundle p0)
  {
    n_onCreate (p0);
  }

  private native void n_onCreate (android.os.Bundle p0);
}

参与注册的代码是类的静态构造函数。 对于为类型注册的每个方法(即在托管代码中实现或替代),JCW 生成器都会输出一个字符串,其中包含要注册的类型和方法的相关信息。 每个注册字符串都以换行符结尾,整个序列以空字符串结束。 所有注册字符串都会被串联起来并放入 __md_methods 静态变量中。 然后调用 mono.android.Runtime.register 方法以注册所有方法。

动态注册调用序列

首次构造或访问类型时,系统将对在生成的 Java 类型中声明的所有本机方法进行注册。 此时,Java VM 调用类型的静态构造函数,启动一系列调用,最后将该类型的所有方法都注册到 JNI:

  1. mono.android.Runtime.register 是一个原生方法,在 .NET for Android 的 Java 运行时代码的 Runtime 类中声明,在原生 .NET for Android 运行时中实现。 此方法的用途是准备对 .NET for Android 的托管运行时代码的调用。
  2. Android.Runtime.JNIEnv::RegisterJniNatives 传递要为其注册 Java 方法的托管类型名称,并使用 .NET 反射加载该类型,然后进行调用以缓存该类型。 它以调用 Android.Runtime.AndroidTypeManager::RegisterNativeMembers 方法结束。
  3. Android.Runtime.AndroidTypeManager::RegisterNativeMembers 调用 Java.Interop.JniEnvironment.Types::RegisterNatives 方法,该方法首先使用 System.Reflection.Emit 生成本机回调方法的委托,然后调用 Java JNI 的 RegisterNatives 函数为托管类型注册本机方法。

注意

System.Reflection.Emit 调用是一个成本高昂的操作,需要对每个注册方法重复执行。

有关 Java 类型注册的详细信息,请参阅Java 类型注册

封送方法

“封送方法注册”方法利用了 JNIS 在本机库中查找 nativeJava 方法的实现的能力。 这些符号的名称必须遵循一系列规则,以便 JNI 能够找到它们。

此方法的目的是绕过动态注册方法,代之以在应用生成期间生成和编译的本机代码。 这减少了应用的启动时间。 为了实现此目标,此方法使用可生成本机代码修改包含注册的方法的程序集的类。

封送方法分类器可识别标准方法注册模式,其中包括:

  • 连接器方法,即注册中的示例中的 GetOnCreate_Landroid_os_Bundle_Handler
  • 委托支持字段,即注册中的示例中的 cb_onCreate_Landroid_os_Bundle_
  • 本机回调方法,即注册中的示例中的 n_OnCreate_Landroid_os_Bundle_
  • 将调用调度到实际对象的虚拟目标方法,即注册中的示例中的 OnCreate

每当分类器运行时,都会向其传递声明类型的方法及其 Register 特性实例,用于检查要注册的方法是否符合注册模式。 连接器、本机回调方法和支持字段必须是专用的和静态的,这样注册的方法才会被视为静态注册的候选项。

注意

未遵循标准模式的注册的方法将以动态方式注册。

使用封送方法时,将为 Java 可调用包装器中显示的 C# 示例生成以下 Java 代码(为清楚起见,省略了某些生成的方法):

public class MainActivity extends androidx.appcompat.app.AppCompatActivity
{
  public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3)
  {
    return n_onCreateView (p0, p1, p2, p3);
  }

  private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3);

  public void onCreate (android.os.Bundle p0)
  {
    n_onCreate (p0);
  }

  private native void n_onCreate (android.os.Bundle p0);
}

与为动态注册生成的代码相比,没有静态构造函数。 但是,代码的其余部分是相同的。

JNI 要求

JNI 指定一系列规则,用于控制本机符号名称的构造方式。 本机方法名称由以下组成部分串联而成:

  • 每个符号的开头都带有 Java_ 前缀。
  • 一个重整的完全限定的类名称
  • 一个用作分隔符的 _ 字符。
  • 一个重整的方法名称
  • (可选)双下划线 __ 和重整的方法参数签名。

重整是一种对某些字符进行编码的方法,这些字符不能直接在源代码和本机符号名称中直接表示。 JNI 规范允许直接使用 ASCII 字母(大写和小写)和数字,而所有其他字符要么用占位符表示,要么编码为 16 位十六进制 Unicode 字符代码:

转义序列 表示
_0XXXX Unicode 字符 XXXX,全部小写。
_1 字符 _
_2 签名中的 ; 字符。
_3 签名中的 [ 字符。
_ ./ 字符。

JNI 符号名称的生成是在生成原生函数源代码时由一个 .NET for Android 生成任务执行的。

JNI 支持本机符号名称的长短格式。 Java VM 会首先查找短格式,然后查找长格式。 长格式仅可用于重载方法。

LLVM 中间表示形式代码生成

一个 .NET for Android 生成任务使用 LLVM 中间表示形式 (IR) 生成器基础结构来输出所有封送方法包装器的数据和可执行代码。 请看下面的 C++ 代码,该代码可用作了解封送方法运行时调用工作原理的指南:

using get_function_pointer_fn = void(*)(uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr);

static get_function_pointer_fn get_function_pointer;

void xamarin_app_init (get_function_pointer_fn fn) noexcept
{
  get_function_pointer = fn;
}

using android_app_activity_on_create_bundle_fn = void (*) (JNIEnv *env, jclass klass, jobject savedInstanceState);
static android_app_activity_on_create_bundle_fn android_app_activity_on_create_bundle = nullptr;

extern "C" JNIEXPORT void
JNICALL Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 (JNIEnv *env, jclass klass, jobject savedInstanceState) noexcept
{
  if (android_app_activity_on_create_bundle == nullptr) {
    get_function_pointer (
      16, // mono image index
      0,  // class index
      0x0600055B, // method token
      reinterpret_cast<void*&>(android_app_activity_on_create_bundle) // target pointer
    );
  }

  android_app_activity_on_create_bundle (env, klass, savedInstanceState);
}

xamarin_app_init 函数仅被输出一次,在应用启动期间由 .NET for Android 运行时调用两次。 第一次调用它是用于传递 get_function_pointer_fn,第二次调用它是在将控制权移交给 Mono VM 之前,以将指针传递至 get_function_pointer_fn

Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 函数是一个模板,会重复用于每个 Java 本机函数,每个函数都有其自己的参数集和回调支持字段(此处为 android_app_activity_on_create_bundle)。

get_function_pointer 函数将索引作为参数引入多个表。 一个用于 MonoImage* 指针,另一个用于 MonoClass* 指针,两者都是在生成应用时生成的,允许在运行时进行非常快速的查找。 检索目标方法时,会根据其令牌值,在指定的 MonoImage*(实质上是指向内存中托管程序集映像的指针)和类中进行检索。

对于以这种方式标识的方法,必须使用 UnmanagedCallersOnly 特性在托管代码中对其进行修饰,以便可以直接调用它,就像它本身就是本机方法一样,并将托管封送开销降到最低。

程序集重写

托管程序集(包括包含 Java 类型的 Mono.Android.dll)需要适合用于动态注册和封送方法。 但是,这两种方法有不同的要求。 无法假定每个程序集都具有适合封送方法的代码。 因此,需要有一种方法来确保代码满足此要求。 这是通过读取程序集并修改它们来实现的,方法是更改本机回调的定义,并删除封送方法不再使用的代码。 此任务由一个 .NET for Android 生成任务执行,该生成任务在应用生成期间调用,且调用时间在链接所有程序集之后但生成类型映射之前。

所应用的具体修改如下:

  • 删除“连接器支持字段”
  • 删除“连接器方法”
  • 生成“本机回调包装器”方法,该方法将捕获和传播本机回调或目标方法引发的未经处理的异常。 会使用 UnmanagedCallersOnly 特性对此方法进行修饰,并且会直接从本机代码调用此方法。
  • 可以选择在本机回调包装器中生成代码来处理非 blittable 类型

修改后,程序集包含每个封送方法的等效 C# 代码,如下所示:

public class MainActivity : AppCompatActivity
{
    // Native callback
    static void n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
    {
        var __this = global::Java.Lang.Object.GetObject<Android.App.Activity>(jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
        var savedInstanceState = global::Java.Lang.Object.GetObject<Android.OS.Bundle>(native_savedInstanceState, JniHandleOwnership.DoNotTransfer);
        __this.OnCreate(savedInstanceState);
    }

    // Native callback exception wrapper
    [UnmanagedCallersOnly]
    static void n_OnCreate_Landroid_os_Bundle__mm_wrapper(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
    {
        try
        {
            n_OnCreate_Landroid_os_Bundle_(jnienv, native__this, native_savedInstanceState)
        }
        catch (Exception ex)
        {
            Android.Runtime.AndroidEnvironmentInternal.UnhandledException(ex);
        }
    }

    // Target method
    [Register("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]
    protected virtual unsafe void OnCreate(Android.OS.Bundle? savedInstanceState)
    {
        const string __id = "onCreate.(Landroid/os/Bundle;)V";
        try
        {
            JniArgumentValue* __args = stackalloc JniArgumentValue[1];
            __args[0] = new JniArgumentValue((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object)savedInstanceState).Handle);
            _members.InstanceMethods.InvokeVirtualVoidMethod(__id, this, __args);
        }
        finally
        {
            global::System.GC.KeepAlive(savedInstanceState);
        }
    }
}

适用于具有非 blittable 类型的方法的包装器

UnmanagedCallersOnly 特性要求所有参数类型和方法返回类型都属于 blittable 类型。 在这些类型中,bool 类型是托管类通常用于实现 Java 方法的类型。 这是目前在绑定中遇到的唯一非 blittable 类型,因此是程序集重写工具唯一支持的类型。

每当遇到具有非 blittable 类型的方法时,都会为其生成包装器,以便可以使用 UnmanagedCallersOnly 特性对其进行修饰。 相较于修改本机回调方法的 IL 流来实现必要的转换而言,该方法更不容易出错。 这种方法的一个示例是 Android.Views.View.IOnTouchListener::OnTouch

static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent(IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e)
{
    var __this = global::Java.Lang.Object.GetObject<Android.Views.View.IOnTouchListener>(jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
    var v = global::Java.Lang.Object.GetObject<Android.Views.View>(native_v, JniHandleOwnership.DoNotTransfer);
    var e = global::Java.Lang.Object.GetObject<Android.Views.MotionEvent>(native_e, JniHandleOwnership.DoNotTransfer);
    bool __ret = __this.OnTouch(v, e);
    return __ret;
}

此方法会返回一个 bool 值,因此需要使用包装器来正确转换返回值。 每个包装器方法会保留本机回调方法名称,但向其追加 _mm_wrapper 后缀:

[UnmanagedCallersOnly]
static byte n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent__mm_wrapper(IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e)
{
    try
    {
        return n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_(jnienv, native__this, native_v, native_e) ? 1 : 0;
    }
    catch (Exception ex)
    {
        Android.Runtime.AndroidEnvironmentInternal.UnhandledException(ex);
        return default;
    }
}

包装器的返回语句使用三元运算符将布尔值强制转换为 1 或 0,因为整个托管运行时中的 bool 值可以采用一系列值(用于 false0、用于 true-11 以及用于 true!= 0)。

由于 C# 中的 bool 类型长度可以是 1、2 或 4 个字节,因此要将其转换为已知的静态大小的类型。 之所以使用 byte 托管类型,是因为它与 Java/JNI jboolean 类型相对应,后者定义为无符号 8 位类型。

每当需要在 bytebool 之间转换参数值时,都会生成与 argument != 0 比较等效的代码。 例如,对于 Android.Views.View.IOnFocusChangeListener::OnFocusChange 方法而言:

[UnmanagedCallersOnly]
static void n_OnFocusChange_Landroid_view_View_Z(IntPtr jnienv, IntPtr native__this, IntPtr native_v, byte hasFocus)
{
    n_OnFocusChange_Landroid_view_View_Z(jnienv, native__this, native_v, hasFocus != 0);
}

static void n_OnFocusChange_Landroid_view_View_Z(IntPtr jnienv, IntPtr native__this, IntPtr native_v, bool hasFocus)
{
    var __this = global::Java.Lang.Object.GetObject<Android.Views.View.IOnFocusChangeListener>(jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
    var v = global::Java.Lang.Object.GetObject<Android.Views.View>(native_v, JniHandleOwnership.DoNotTransfer);
    __this.OnFocusChange(v, hasFocus);
}

UnmanagedCallersOnly 特性

会使用 UnmanagedCallersOnly 特性对每个封送方法本机回调方法进行修饰,以便能够以最小的开销直接从本机代码调用回调。

封送方法注册调用序列

动态和封送方法注册有一个共同点,那就是由 Java VM 运行时对本机函数目标执行解析。 在这两种情况下,Java VM 在首次对代码进行 JIT 处理时,都会查找在 Java 类中声明为 native 的方法。 区别在于此查找的执行方式。

动态注册在运行时使用 RegisterNatives JNI 函数,该函数将注册的方法的指针存储在 Java VM 中描述 Java 类的结构内。

但是,封送方法不会向 JNI 注册任何内容。 实际上,它们依赖于 Java VM 的符号查找方法。 每当对 nativeJava 方法调用进行 JIT 处理,且该调用之前不是使用 RegisterNatives JNI 函数注册的时候,Java VM 将继续在进程运行时映像中查找符号,并在找到匹配的符号后,使用指向它的指针作为 nativeJava 方法调用的目标。