使用 JNI 和 Xamarin.Android

Xamarin.Android 允許使用 C# 撰寫 Android 應用程式,而不是使用 Java。 有數個元件隨附 Xamarin.Android,可提供 Java 連結庫的系結,包括Mono.Android.dll和 Mono.Android.Google 地圖.dll。 不過,不會針對每個可能的 Java 連結庫提供系結,而提供的系結可能不會繫結每個 Java 類型和成員。 若要使用未系結的 Java 類型和成員,可以使用 Java Native Interface (JNI)。 本文說明如何使用 JNI 與 Xamarin.Android 應用程式中的 Java 類型和成員互動。

概觀

不一定有必要或可能建立 Managed 可呼叫包裝函式 (MCW) 來叫用 Java 程式代碼。 在許多情況下,「內嵌」JNI 非常適合使用未系結的 Java 成員。 使用 JNI 在 Java 類別上叫用單一方法通常比產生整個.jar系結更簡單。

Xamarin.Android 提供 Mono.Android.dll 元件,提供 Android 連結庫的 android.jar 系結。 在 中 Mono.Android.dll 不存在的類型和不存在的類型 android.jar ,都可以透過手動系結它們來使用。 若要系結 Java 類型和成員,您可以使用 Java 原生介面JNI) 來查閱類型、讀取和寫入字段,以及叫用方法。

Xamarin.Android 中的 JNI API 在概念上非常類似於 System.Reflection .NET 中的 API:它可讓您依名稱、讀取和寫入域值、叫用方法等等來查閱類型和成員。 您可以使用 JNI 和 Android.Runtime.RegisterAttribute 自訂屬性來宣告可繫結以支援覆寫的虛擬方法。 您可以系結介面,使其可以在 C# 中實作。

本檔案說明:

  • JNI 如何參考類型。
  • 如何查閱、讀取和寫入欄位。
  • 如何查閱和叫用方法。
  • 如何公開虛擬方法以允許從Managed程式碼覆寫。
  • 如何公開介面。

需求

透過 Android.Runtime.JNIEnv 命名空間公開的 JNI,可在每個版本的 Xamarin.Android 中使用。 若要系結 Java 類型和介面,您必須使用 Xamarin.Android 4.0 或更新版本。

Managed 可呼叫包裝函式

Managed 可呼叫包裝函式 (MCW) 是 Java 類別或介面的系結,它會包裝所有 JNI 機械,讓用戶端 C# 程式代碼不需要擔心 JNI 的基礎複雜度。 Mono.Android.dll大部分是由Managed可呼叫包裝函式所組成。

Managed 可呼叫包裝函式有兩個用途:

  1. 封裝 JNI 使用,讓用戶端程式代碼不需要知道基礎複雜性。
  2. 讓子類別 Java 類型能夠實作 Java 介面。

第一個目的純粹是為了方便和封裝複雜度,讓取用者有一組簡單的受控類別可供使用。 這需要使用本文稍後所述的各種 JNIEnv 成員。 請記住,受控可呼叫包裝函式並非絕對必要 –「內嵌」JNI 使用是完全可接受的,而且對於一次性使用未系結的Java成員很有用。 子類別化和介面實作需要使用Managed可呼叫包裝函式。

Android 可呼叫包裝函式

每當 Android 執行時間 (ART) 需要叫用 Managed 程式代碼時,都需要 Android 可呼叫包裝函式 (ACW) ;這些包裝函式是必要的,因為在運行時間無法向 ART 註冊類別。 (具體來說,是 Android 運行時間不支援 DefineClass JNI 函式。因此,Android 可呼叫包裝函式彌補了缺乏運行時間類型註冊支援。

每當 Android 程式代碼需要執行在 Managed 程式代碼中覆寫或實作的虛擬或介面方法時,Xamarin.Android 必須提供 Java Proxy,讓此方法分派至適當的 Managed 類型。 這些 Java Proxy 類型是 Java 程式代碼,其基類和 Java 介面清單與 Managed 類型相同,實作相同的建構函式,並宣告任何覆寫的基類和介面方法。

Android 可呼叫包裝函式會在建置程式期間由monodroid.exe程序產生,並針對繼承 Java.Lang.Object 的所有類型產生。

實作介面

有時候您可能需要實作 Android 介面(例如 Android.Content.IComponentCallbacks)。

所有 Android 類別和介面都會 擴充 Android.Runtime.IJavaObject 介面;因此,所有 Android 類型都必須實作 IJavaObject。 Xamarin.Android 會利用這個事實 –它用來 IJavaObject 為 Android 提供給定 Managed 類型的 Java Proxy(Android 可呼叫包裝函式)。 因為monodroid.exe只會尋找Java.Lang.Object子類別 (必須實IJavaObject作 ),因此子類別Java.Lang.Object化提供一種方式,以在Managed程式碼中實作介面。 例如:

class MyComponentCallbacks : Java.Lang.Object, Android.Content.IComponentCallbacks {
    public void OnConfigurationChanged (Android.Content.Res.Configuration newConfig) {
        // implementation goes here...
    }
    public void OnLowMemory () {
        // implementation goes here...
    }
}

實作詳細資料

本文的其餘部分提供實作詳細數據,但不會通知 變更(這裡只會顯示,因為開發人員可能會對幕後發生的事情感到好奇)。

例如,假設有下列 C# 來源:

using System;
using Android.App;
using Android.OS;

namespace Mono.Samples.HelloWorld
{
    public class HelloAndroid : Activity
    {
        protected override void OnCreate (Bundle savedInstanceState)
        {
            base.OnCreate (savedInstanceState);
            SetContentView (R.layout.main);
        }
    }
}

mandroid.exe程式會產生下列 Android 可呼叫包裝函式:

package mono.samples.helloWorld;

public class HelloAndroid extends android.app.Activity {
    static final String __md_methods;
    static {
        __md_methods =
            "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" +
            "";
        mono.android.Runtime.register (
                "Mono.Samples.HelloWorld.HelloAndroid, HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                HelloAndroid.class,
                __md_methods);
    }

    public HelloAndroid ()
    {
        super ();
        if (getClass () == HelloAndroid.class)
            mono.android.TypeManager.Activate (
                "Mono.Samples.HelloWorld.HelloAndroid, HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                "", this, new java.lang.Object[] { });
    }

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

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

請注意,會保留基類,而且會針對 Managed 程式代碼內覆寫的每個方法提供原生方法宣告。

ExportAttribute 和 ExportFieldAttribute

一般而言,Xamarin.Android 會自動產生由 ACW 組成的 Java 程式代碼;當類別衍生自 Java 類別並覆寫現有的 Java 方法時,這個世代會以 類別和方法名稱為基礎。 不過,在某些情況下,產生程式代碼並不夠充分,如下所述:

  • Android 支援配置 XML 屬性中的動作名稱,例如 android:onClick XML 屬性。 指定時,擴充的 View 實例會嘗試查閱 Java 方法。

  • java.io.Serializable 介面需要 readObjectwriteObject 方法。 由於它們不是這個介面的成員,因此我們的對應 Managed 實作不會將這些方法公開給 Java 程式代碼。

  • android.os.Parcelable 介面預期實作類別必須具有 類型的Parcelable.Creator靜態字段CREATOR。 產生的 Java 程式代碼需要一些明確的欄位。 在我們的標準案例中,無法從 Managed 程式代碼輸出 Java 程式代碼中的欄位。

因為程式代碼產生不提供解決方案來產生任意名稱的任意 Java 方法,因此從 Xamarin.Android 4.2 開始,引進 ExportAttribute 和 ExportFieldAttribute 來提供上述案例的解決方案。 這兩個屬性都位於 命名空間中 Java.Interop

  • ExportAttribute – 指定方法名稱和其預期的例外狀況類型(以在 Java 中提供明確的「擲回」)。 在方法上使用此方法時,方法會「導出」Java 方法,以產生分派程式代碼給 Managed 方法的對應 JNI 調用。 這可以與 和java.io.Serializable搭配android:onClick使用。

  • ExportFieldAttribute – 指定功能變數名稱。 它位於做為欄位初始化運算式的方法上。 這可以搭配 android.os.Parcelable使用。

針對 ExportAttribute 和 ExportFieldAttribute 進行疑難解答

  • 封裝因遺漏 Mono.Android.Export.dll 而失敗 – 如果您在程式代碼或相依連結庫的某些方法上使用 ExportAttributeExportFieldAttribute ,則必須新增 Mono.Android.Export.dll。 此元件會隔離,以支援 Java 的回呼程式代碼。 它與 Mono.Android.dll 分開,因為它會將額外的大小新增至應用程式。

  • 在 [發行組建] 中, MissingMethodException 針對 Export 方法發生 – 在發行組建中, MissingMethodException 會針對 Export 方法發生。 (此問題已在最新版本的 Xamarin.Android 中修正。

ExportParameterAttribute

ExportAttribute 並提供 ExportFieldAttribute Java 執行時間程式代碼可以使用的功能。 此運行時間程式代碼會透過這些屬性所驅動產生的 JNI 方法存取 Managed 程式代碼。 因此,Managed 方法不會系結現有的Java方法;因此,Java 方法會從 Managed 方法簽章產生。

不過,此案例並不完全具決定性。 最值得注意的是,在 Managed 類型和 Java 類型之間的一些進階對應中也是如此,例如:

  • InputStream
  • OutputStream
  • XmlPullParser
  • XmlResourceParser

匯出方法需要這類型別時, ExportParameterAttribute 必須使用 來明確提供對應的參數或傳回型別。

註釋屬性

在 Xamarin.Android 4.2 中,我們已將 IAnnotation 實作類型轉換成屬性 (System.Attribute),並在 Java 包裝函式中新增批注產生的支援。

這表示下列方向變更:

  • 系結產生器會產生 Java.Lang.DeprecatedAttributejava.Lang.Deprecated (當它應該在 [Obsolete] Managed程式碼中時)。

  • 這並不表示現有的 Java.Lang.Deprecated 類別將會消失。 這些以 Java 為基礎的物件仍可像往常一樣使用 Java 物件(如果存在這類用法)。 將會有 DeprecatedDeprecatedAttribute 類別。

  • 類別 Java.Lang.DeprecatedAttribute 會標示為 [Annotation] 。 當有繼承自此屬性 [Annotation] 的自定義屬性時,msbuild 工作會在 Android 可呼叫包裝函式 (ACW) 中產生該自定義屬性的 Java 註釋(@Deprecated)。

  • 批註可以產生至類別、方法和導出欄位(這是 Managed 程式代碼中的方法)。

如果未註冊包含類別(批注類別本身或包含批注成員的類別),則不會產生整個 Java 類別來源,包括批注。 針對方法,您可以指定 ExportAttribute 來取得明確產生和標註方法的 。 此外,它不是「產生」Java 註釋類別定義的功能。 換句話說,如果您針對特定註釋定義自定義 Managed 屬性,則必須新增另一個包含對應 Java 註釋類別的.jar連結庫。 新增定義批注類型的 Java 原始程式檔是不夠的。 Java 編譯程式無法以 與apt相同的方式運作。

此外,適用下列限制:

  • 到目前為止,此轉換程式不會考慮 @Target 註釋類型的註釋。

  • 屬性上的屬性無法運作。 請改用屬性 getter 或 setter 的屬性。

類別系結

系結類別表示撰寫 Managed 可呼叫包裝函式,以簡化基礎 Java 類型的叫用。

系結虛擬和抽象方法以允許從 C# 覆寫需要 Xamarin.Android 4.0。 不過,任何版本的 Xamarin.Android 都可以繫結非虛擬方法、靜態方法或虛擬方法,而不需要支援覆寫。

系結通常包含下列專案:

宣告類型句柄

欄位和方法查閱方法需要參考其宣告類型的對象參考。 依照慣例,這會在 class_ref 欄位中舉行:

static IntPtr class_ref = JNIEnv.FindClass(CLASS);

如需令牌的詳細資訊CLASS,請參閱 JNI 類型參考一節。

系結欄位

Java 欄位會公開為 C# 屬性,例如 Java 欄位 java.lang.System.in 系結為 C# 屬性 Java.Lang.JavaSystem.In。 此外,由於 JNI 區分靜態字段和實例字段,因此實作屬性時會使用不同的方法。

欄位系結包含三組方法:

  1. 取得欄位識別碼方法。 get field id 方法負責傳回取得域值設定域值方法將使用的欄位句柄。 取得欄位識別碼需要知道宣告類型、功能變數名稱,以及 欄位的 JNI 類型簽章

  2. 取得域值方法。 這些方法需要欄位句柄,並負責從 Java 讀取欄位的值。 要使用的方法取決於欄位的類型。

  3. 設定 域值 方法。 這些方法需要欄位句柄,並負責在Java中寫入域的值。 要使用的方法取決於欄位的類型。

靜態欄位 使用 JNIEnv.GetStaticFieldIDJNIEnv.GetStatic*FieldJNIEnv.SetStaticField 方法。

實例欄位使用 JNIEnv.GetFieldIDJNIEnv.Get*FieldJNIEnv.SetField 方法。

例如,靜態屬性 JavaSystem.In 可以實作為:

static IntPtr in_jfieldID;
public static System.IO.Stream In
{
    get {
        if (in_jfieldId == IntPtr.Zero)
            in_jfieldId = JNIEnv.GetStaticFieldID (class_ref, "in", "Ljava/io/InputStream;");
        IntPtr __ret = JNIEnv.GetStaticObjectField (class_ref, in_jfieldId);
        return InputStreamInvoker.FromJniHandle (__ret, JniHandleOwnership.TransferLocalRef);
    }
}

注意:我們使用 InputStreamInvoker.FromJniHandle 將 JNI 參考 System.IO.Stream 轉換成實例,而我們正在使用 JniHandleOwnership.TransferLocalRef ,因為 JNIEnv.GetStaticObjectField 會傳回本機參考。

許多 Android.Runtime 類型都有FromJniHandle方法,可將 JNI 參考轉換成所需的類型。

方法系結

Java 方法會公開為 C# 方法和 C# 屬性。 例如,Java 方法 java.lang.Runtime.runFinalizersOnExit 方法會系結為 Java.Lang.Runtime.RunFinalizersOnExit 方法,而 java.lang.Object.getClass 方法會系結為 Java.Lang.Object.Class 屬性。

方法調用是兩個步驟的程式:

  1. 叫用之方法的 get 方法標識符get 方法 id 方法負責傳回方法調用方法將使用的方法句柄。 取得方法標識碼需要知道宣告類型、方法的名稱,以及 方法的 JNI 類型簽章

  2. 叫用方法。

就像字段一樣,用來取得方法標識符和叫用方法的方法在靜態方法和實例方法之間有所不同。

靜態方法 會使用 JNIEnv.GetStaticMethodID() 來查閱方法標識碼,並使用 JNIEnv.CallStatic*Method 方法系列進行調用。

實例方法 會使用 JNIEnv.GetMethodID 來查閱方法標識符,並使用 JNIEnv.Call*MethodJNIEnv.CallNonvirtual*Method 方法系列進行調用。

方法系結可能不僅僅是方法調用。 方法系結也包含允許覆寫方法(適用於抽象和非最終方法)或實作的方法(適用於介面方法)。 [ 支持繼承, 介面 ] 區段涵蓋支持虛擬方法和介面方法的複雜性。

靜態方法

系結靜態方法牽涉到使用 JNIEnv.GetStaticMethodID 來取得方法句柄,然後使用適當的 JNIEnv.CallStatic*Method 方法,視方法的傳回型別而定。 以下是 Runtime.getRuntime 方法的系結範例:

static IntPtr id_getRuntime;

[Register ("getRuntime", "()Ljava/lang/Runtime;", "")]
public static Java.Lang.Runtime GetRuntime ()
{
    if (id_getRuntime == IntPtr.Zero)
        id_getRuntime = JNIEnv.GetStaticMethodID (class_ref,
                "getRuntime", "()Ljava/lang/Runtime;");

    return Java.Lang.Object.GetObject<Java.Lang.Runtime> (
            JNIEnv.CallStaticObjectMethod  (class_ref, id_getRuntime),
            JniHandleOwnership.TransferLocalRef);
}

請注意,我們會將方法句柄儲存在靜態欄位中。 id_getRuntime 這是效能優化,因此不需要查詢每個叫用的方法句柄。 不需要以這種方式快取方法句柄。 取得方法句柄之後, 會使用 JNIEnv.CallStaticObjectMethod 來叫用 方法。 JNIEnv.CallStaticObjectMethod 會傳 IntPtr 回 ,其中包含所傳回 Java 實例的句柄。 Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) 用來將 Java 句柄轉換成強型別物件實例。

非虛擬實例方法系結

final系結實例方法,或不需要覆寫的實例方法,牽涉到使用 JNIEnv.GetMethodID 取得方法句柄,然後使用適當的JNIEnv.Call*Method方法,視方法的傳回型別而定。 以下是 屬性的系結 Object.Class 範例:

static IntPtr id_getClass;
public Java.Lang.Class Class {
    get {
        if (id_getClass == IntPtr.Zero)
            id_getClass = JNIEnv.GetMethodID (class_ref, "getClass", "()Ljava/lang/Class;");
        return Java.Lang.Object.GetObject<Java.Lang.Class> (
                JNIEnv.CallObjectMethod (Handle, id_getClass),
                JniHandleOwnership.TransferLocalRef);
    }
}

請注意,我們會將方法句柄儲存在靜態欄位中。 id_getClass 這是效能優化,因此不需要查詢每個叫用的方法句柄。 不需要以這種方式快取方法句柄。 取得方法句柄之後, 會使用 JNIEnv.CallStaticObjectMethod 來叫用 方法。 JNIEnv.CallStaticObjectMethod 會傳 IntPtr 回 ,其中包含所傳回 Java 實例的句柄。 Java.Lang.Object.GetObject<T>(IntPtr, JniHandleOwnership) 用來將 Java 句柄轉換成強型別物件實例。

系結建構函式

建構函式是名稱為的 "<init>"Java 方法。 就像使用 Java 實例方法一樣, JNIEnv.GetMethodID 用來查閱建構函式句柄。 與 Java 方法不同, JNIEnv.NewObject 方法可用來叫用建構函式方法句柄。 的傳回值為 JNIEnv.NewObject JNI 本機參考:

int value = 42;
IntPtr class_ref    = JNIEnv.FindClass ("java/lang/Integer");
IntPtr id_ctor_I    = JNIEnv.GetMethodID (class_ref, "<init>", "(I)V");
IntPtr lrefInstance = JNIEnv.NewObject (class_ref, id_ctor_I, new JValue (value));
// Dispose of lrefInstance, class_ref…

一般而言,類別系結將會是子類別 Java.Lang.Object。 子類別化 Java.Lang.Object時,額外的語意會開始執行: Java.Lang.Object 實例會透過 屬性維護 Java 實例的 Java.Lang.Object.Handle 全域參考。

  1. 默認建 Java.Lang.Object 構函式會配置 Java 實例。

  2. 如果類型具有 RegisterAttribute , 且 RegisterAttribute.DoNotGenerateAcwtrue ,則類型實例 RegisterAttribute.Name 會透過其預設建構函式建立。

  3. 否則,對應至的 this.GetType Android 可呼叫包裝函式 (ACW) 會透過其預設建構函式具現化。 Android 可呼叫包裝函式會在套件建立期間針對未設定為 true的每個Java.Lang.Object子類別RegisterAttribute.DoNotGenerateAcw產生。

對於不是類別系結的類型,這是預期的語意:具現 Mono.Samples.HelloWorld.HelloAndroid 化 C# 實例應該建構 mono.samples.helloworld.HelloAndroid Java 實例,這是產生的 Android 可呼叫包裝函式。

針對類別系結,如果 Java 類型包含預設建構函式,且/或不需要叫用其他建構函式,這可能是正確的行為。 否則,必須提供會執行下列動作的建構函式:

  1. 用 Java.Lang.Object(IntPtr,JniHandleOwnership) 而不是預設 Java.Lang.Object 建構函式。 這需要避免建立新的 Java 實例。

  2. 建立任何 Java 實例之前,請先檢查 Java.Lang.Object.Handle 的值。 如果 Android 可呼叫包裝函式是在 Java 程式代碼中建構,而且正在建構類別系結以包含已建立的 Android 可呼叫包裝函式實例,則 Object.Handle 屬性會具有以外的 IntPtr.Zero 值。 例如,當 Android 建立 mono.samples.helloworld.HelloAndroid 實例時,系統會先建立 Android 可呼叫包裝函式,而 Java HelloAndroid 建構函式會建立對應 Mono.Samples.HelloWorld.HelloAndroid 類型的實例, Object.Handle 並在建構函式執行之前將 屬性設定為 Java 實例。

  3. 如果目前的運行時間類型與宣告類型不同,則必須建立對應 Android 可呼叫包裝函式的實例,並使用 Object.SetHandle 來儲存 JNIEnv.CreateInstance回的句柄。

  4. 如果目前的運行時間類型與宣告類型相同,則叫用 Java建構函式並使用Object.SetHandle 來儲存 所傳回的 JNIEnv.NewInstance 句柄。

例如,請考慮 java.lang.Integer(int) 建構函式。 這會系結為:

// Cache the constructor's method handle for later use
static IntPtr id_ctor_I;

// Need [Register] for subclassing
// RegisterAttribute.Name is always ".ctor"
// RegisterAttribute.Signature is tye JNI type signature of constructor
// RegisterAttribute.Connector is ignored; use ""
[Register (".ctor", "(I)V", "")]
public Integer (int value)
    // 1. Prevent Object default constructor execution
    : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
{
    // 2. Don't allocate Java instance if already allocated
    if (Handle != IntPtr.Zero)
        return;

    // 3. Derived type? Create Android Callable Wrapper
    if (GetType () != typeof (Integer)) {
        SetHandle (
                Android.Runtime.JNIEnv.CreateInstance (GetType (), "(I)V", new JValue (value)),
                JniHandleOwnership.TransferLocalRef);
        return;
    }

    // 4. Declaring type: lookup &amp; cache method id...
    if (id_ctor_I == IntPtr.Zero)
        id_ctor_I = JNIEnv.GetMethodID (class_ref, "<init>", "(I)V");
    // ...then create the Java instance and store
    SetHandle (
            JNIEnv.NewObject (class_ref, id_ctor_I, new JValue (value)),
            JniHandleOwnership.TransferLocalRef);
}

JNIEnv.CreateInstance 方法是協助程式,可對從 JNIEnv.FindClass傳回的值執行 JNIEnv.FindClassJNIEnv.GetMethodIDJNIEnv.NewObjectJNIEnv.DeleteGlobalReference 。 如需詳細資訊,請參閱下一節。

支援繼承、介面

子類別化 Java 類型或實作 Java 介面需要產生封裝程式期間針對每個Java.Lang.Object子類別產生的 Android 可呼叫包裝函式 (ACWs)。 ACW 產生是透過 Android.Runtime.RegisterAttribute 自定義屬性來控制。

對於 C# 類型,自定義屬性建 [Register] 構函式需要一個自變數: 對應 Java 類型的 JNI 簡化型別參考 。 這允許在 Java 和 C# 之間提供不同的名稱。

在 Xamarin.Android 4.0 之前, [Register] 自定義屬性無法「別名」現有的 Java 類型使用。 這是因為 ACW 產生程式會產生每個 Java.Lang.Object 遇到子類別的 ACW。

Xamarin.Android 4.0 引進 RegisterAttribute.DoNotGenerateAcw 屬性。 這個屬性會指示 ACW 產生程式 略過 批注型別,允許宣告新的 Managed 可呼叫包裝函式,這不會在封裝建立時產生 ACW。 這允許系結現有的 Java 類型。 例如,請考慮下列簡單的 Java 類別, Adder其中包含一個方法, add它會新增至整數並傳回結果:

package mono.android.test;
public class Adder {
    public int add (int a, int b) {
        return a + b;
    }
}

類型 Adder 可以系結為:

[Register ("mono/android/test/Adder", DoNotGenerateAcw=true)]
public partial class Adder : Java.Lang.Object {
    static IntPtr class_ref = JNIEnv.FindClass ( "mono/android/test/Adder");

    public Adder ()
    {
    }

    public Adder (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
    }
}
partial class ManagedAdder : Adder {
}

在這裡, Adder C# 類型 別名為Adder Java 類型。 屬性 [Register] 是用來指定 Java 類型的 JNI 名稱 mono.android.test.Adder ,而 DoNotGenerateAcw 屬性則用來抑制 ACW 產生。 這會導致類型產生 ManagedAdder ACW,其適當地將類型子類別 mono.android.test.Adder 化。 RegisterAttribute.DoNotGenerateAcw如果尚未使用 屬性,則 Xamarin.Android 建置程式會產生新的 mono.android.test.Adder Java 類型。 這會導致編譯錯誤,因為 mono.android.test.Adder 類型會在兩個不同的檔案中出現兩次。

系結虛擬方法

ManagedAdder 子類別化 Java Adder 類型,但並不特別有趣:C# Adder 類型不會定義任何虛擬方法,因此 ManagedAdder 無法覆寫任何專案。

若要允許子類別覆寫的系 virtual 結方法,需要完成數項工作,這分為下列兩個類別:

  1. 方法系結

  2. 方法註冊

方法系結

方法系結需要將兩個支援成員新增至 C# Adder 定義: ThresholdType、 和 ThresholdClass

ThresholdType

屬性 ThresholdType 會傳回系結的目前類型:

partial class Adder {
    protected override System.Type ThresholdType {
        get {
            return typeof (Adder);
        }
    }
}

ThresholdType 用於方法系結,以判斷何時應該執行虛擬與非虛擬方法分派。 它應該一律傳回 System.Type 對應至宣告 C# 類型的實例。

ThresholdClass

屬性 ThresholdClass 會傳回系結類型的 JNI 類別參考:

partial class Adder {
    protected override IntPtr ThresholdClass {
        get {
            return class_ref;
        }
    }
}

ThresholdClass 在叫用非虛擬方法時,會在方法系結中使用。

系結實作

方法系結實作負責Java方法的運行時間調用。 它也包含 [Register] 屬於方法註冊一部分的自定義屬性宣告,並將在方法註冊一節中討論:

[Register ("add", "(II)I", "GetAddHandler")]
    public virtual int Add (int a, int b)
    {
        if (id_add == IntPtr.Zero)
            id_add = JNIEnv.GetMethodID (class_ref, "add", "(II)I");
        if (GetType () == ThresholdType)
            return JNIEnv.CallIntMethod (Handle, id_add, new JValue (a), new JValue (b));
        return JNIEnv.CallNonvirtualIntMethod (Handle, ThresholdClass, id_add, new JValue (a), new JValue (b));
    }
}

欄位 id_add 包含要叫用之 Java 方法的方法識別碼。 從 id_add 取得 JNIEnv.GetMethodID值,其需要宣告類別 (class_ref)、Java 方法名稱 ("add"), 和 方法的 JNI 簽章 ("(II)I")。

取得方法標識碼之後, GetType 會比較以 ThresholdType 判斷是否需要虛擬或非虛擬分派。 比對 ThresholdTypeGetType,需要虛擬分派,例如Handle參考覆寫 方法的Java配置子類別。

當 不符合 時GetTypeAdder已經子類別化 (例如 by ManagedAdder),而且只有在叫用子類別base.Add時,才會叫用 實Adder.Add作。ThresholdType 這是非虛擬分派案例,也就是傳入的位置 ThresholdClassThresholdClass 指定要叫用之方法的實作 Java 類別。

方法註冊

假設我們有可覆寫 方法的Adder.Add更新ManagedAdder定義:

partial class ManagedAdder : Adder {
    public override int Add (int a, int b) {
        return (a*2) + (b*2);
    }
}

回想一下[Register]Adder.Add具有自定義屬性:

[Register ("add", "(II)I", "GetAddHandler")]

自訂屬性建 [Register] 構函式接受三個值:

  1. 在此案例中, "add" Java 方法的名稱。

  2. 在此情況下, "(II)I" 方法的 JNI 類型簽章。

  3. 連接器方法GetAddHandler,在此案例中為 。 稍後將討論 連線 或方法。

前兩個參數可讓 ACW 產生程式產生方法宣告來覆寫 方法。 產生的 ACW 將包含下列一些程式代碼:

public class ManagedAdder extends mono.android.test.Adder {
    static final String __md_methods;
    static {
        __md_methods = "n_add:(II)I:GetAddHandler\n" +
            "";
        mono.android.Runtime.register (...);
    }
    @Override
    public int add (int p0, int p1) {
        return n_add (p0, p1);
    }
    private native int n_add (int p0, int p1);
    // ...
}

請注意, @Override 宣告方法,它會委派給 n_相同名稱的前置方法。 這可確保當 Java 程式代碼叫ManagedAdder.addManagedAdder.n_add用 時,將會叫用 ,以允許執行覆寫的 C# ManagedAdder.Add 方法。

因此,最重要的問題:如何 ManagedAdder.n_add 連結 ManagedAdder.Add至 ?

Java native 方法會透過 JNI RegisterNatives 函式向 Java(Android 執行時間)運行時間註冊。 RegisterNatives會採用結構陣列,其中包含Java方法名稱、JNI 類型簽章,以及要叫用且遵循 JNI 呼叫慣例函式指標。 函式指標必須是接受兩個指標自變數的函式,後面接著方法參數。 Java ManagedAdder.n_add 方法必須透過具有下列 C 原型的函式來實作:

int FunctionName(JNIEnv *env, jobject this, int a, int b)

Xamarin.Android 不會公開 RegisterNatives 方法。 相反地,ACW 和 MCW 會同時提供叫 RegisterNatives用所需的資訊:ACW 包含方法名稱和 JNI 類型簽章,唯一缺少的就是要連結的函式指標。

這是連接器方法的所在位置。 第三 [Register] 個自定義屬性參數是註冊型別中定義的方法名稱,或是接受任何參數並傳 System.Delegate回 之已註冊型別的基類。 傳回 System.Delegate 的 會參考具有正確 JNI 函式簽章的方法。 最後,連接器方法傳回 的委派必須 進行根目錄,讓 GC 不會收集它,因為委派會提供給 Java。

#pragma warning disable 0169
static Delegate cb_add;
// This method must match the third parameter of the [Register]
// custom attribute, must be static, must return System.Delegate,
// and must accept no parameters.
static Delegate GetAddHandler ()
{
    if (cb_add == null)
        cb_add = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, int, int, int>) n_Add);
    return cb_add;
}
// This method is registered with JNI.
static int n_Add (IntPtr jnienv, IntPtr lrefThis, int a, int b)
{
    Adder __this = Java.Lang.Object.GetObject<Adder>(lrefThis, JniHandleOwnership.DoNotTransfer);
    return __this.Add (a, b);
}
#pragma warning restore 0169

方法 GetAddHandler 會建立參考 Func<IntPtr, IntPtr, int, int, int> 方法的 n_Add 委派,然後叫用 JNINativeWrapper.CreateDelegateJNINativeWrapper.CreateDelegate 將提供的 方法包裝在 try/catch 區塊中,以便處理任何未處理的例外狀況,並會導致引發 AndroidEvent.UnhandledExceptionRaiser 事件。 產生的委派會儲存在靜態 cb_add 變數中,讓 GC 不會釋放委派。

最後,方法 n_Add 負責將 JNI 參數封送處理至對應的 Managed 型別,然後委派方法呼叫。

注意:一律在透過Java實例取得MCW時使用 JniHandleOwnership.DoNotTransfer 。 將它們視為本機參考(因此呼叫 JNIEnv.DeleteLocalRef)會中斷 Managed -> Java -> 受控堆疊轉換。

完成載入宏系結

類型的完整 Managed 系結 mono.android.tests.Adder 為:

[Register ("mono/android/test/Adder", DoNotGenerateAcw=true)]
public class Adder : Java.Lang.Object {

    static IntPtr class_ref = JNIEnv.FindClass ("mono/android/test/Adder");

    public Adder ()
    {
    }

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

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

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

#region Add
    static IntPtr id_add;

    [Register ("add", "(II)I", "GetAddHandler")]
    public virtual int Add (int a, int b)
    {
        if (id_add == IntPtr.Zero)
            id_add = JNIEnv.GetMethodID (class_ref, "add", "(II)I");
        if (GetType () == ThresholdType)
            return JNIEnv.CallIntMethod (Handle, id_add, new JValue (a), new JValue (b));
        return JNIEnv.CallNonvirtualIntMethod (Handle, ThresholdClass, id_add, new JValue (a), new JValue (b));
    }

#pragma warning disable 0169
    static Delegate cb_add;
    static Delegate GetAddHandler ()
    {
        if (cb_add == null)
            cb_add = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, int, int, int>) n_Add);
        return cb_add;
    }

    static int n_Add (IntPtr jnienv, IntPtr lrefThis, int a, int b)
    {
        Adder __this = Java.Lang.Object.GetObject<Adder>(lrefThis, JniHandleOwnership.DoNotTransfer);
        return __this.Add (a, b);
    }
#pragma warning restore 0169
#endregion
}

限制

撰寫符合下列準則的類型時:

  1. Java.Lang.Object

  2. [Register]具有自訂屬性

  3. RegisterAttribute.DoNotGenerateAcwtrue

然後,針對 GC 互動,類型 不得 有任何字段可以在運行時間參考 Java.Lang.ObjectJava.Lang.Object 子類別。 例如,不允許型 System.Object 別和任何介面類型的欄位。 不允許參考 Java.Lang.Object 實體的類型,例如 System.StringList<int>。 這項限制是防止 GC 過早收集物件。

如果類型必須包含參考實體的 Java.Lang.Object 實體欄位,則字段類型必須是 System.WeakReferenceGCHandle

系結抽象方法

系結 abstract 方法與系結虛擬方法大致相同。 只有兩個差異:

  1. 抽象方法是抽象的。 它仍然會保留 [Register] 屬性和相關聯的方法註冊,方法系結只會移至 Invoker 類型。

  2. 建立非 abstractInvoker 型別,以子類別化抽象型別。 型 Invoker 別必須覆寫基類中宣告的所有抽象方法,而覆寫的實作是方法系結實作,不過可以忽略非虛擬分派案例。

例如,假設上述 mono.android.test.Adder.add 方法為 abstract。 C# 系結會變更為Adder.Add抽象,而且會定義實Adder.Add作 的新AdderInvoker類型:

partial class Adder {
    [Register ("add", "(II)I", "GetAddHandler")]
    public abstract int Add (int a, int b);

    // The Method Registration machinery is identical to the
    // virtual method case...
}

partial class AdderInvoker : Adder {
    public AdderInvoker (IntPtr handle, JniHandleOwnership transfer)
        : base (handle, transfer)
    {
    }

    static IntPtr id_add;
    public override int Add (int a, int b)
    {
        if (id_add == IntPtr.Zero)
            id_add = JNIEnv.GetMethodID (class_ref, "add", "(II)I");
        return JNIEnv.CallIntMethod (Handle, id_add, new JValue (a), new JValue (b));
    }
}

Invoker只有在取得 Java 建立實例的 JNI 參考時,才需要此類型。

系結介面

系結介面在概念上類似於包含虛擬方法的系結類別,但許多細節在細微(且不太微妙)方面有所不同。 請考慮下列 Java 介面宣告

public interface Progress {
    void onAdd(int[] values, int currentIndex, int currentSum);
}

介面系結有兩個部分:C# 介面定義,以及介面的 Invoker 定義。

介面定義

C# 介面定義必須滿足下列需求:

  • 介面定義必須具有 [Register] 自定義屬性。

  • 介面定義必須擴充 IJavaObject interface。 若無法這麼做,將防止 ACW 繼承自 Java 介面。

  • 每個介面方法都必須包含屬性 [Register] ,指定對應的 Java 方法名稱、JNI 簽章和連接器方法。

  • 連接器方法也必須指定連接器方法可以位於的類型。

當系結 abstractvirtual 方法時,連接器方法會在所註冊類型的繼承階層內搜尋。 介面不能有包含主體的方法,因此無法運作,因此需要指定類型,指出連接器方法所在的位置。 型別是在連接器方法字串中指定,在冒號 ':'之後,而且必須是包含叫用者之型別的元件限定型別名稱。

介面方法宣告是使用 相容 類型之對應Java方法的轉譯。 針對 Java 內建類型,相容的類型是對應的 C# 類型,例如 Java int 是 C# int。 針對參考型別,相容類型是一種類型,可提供適當 Java 類型的 JNI 句柄。

Java 不會直接叫用介面成員 – 叫用會透過 Invoker 類型進行媒體處理,因此允許一些彈性。

Java Progress 介面可以在 C# 中宣告為

[Register ("mono/android/test/Adder$Progress", DoNotGenerateAcw=true)]
public interface IAdderProgress : IJavaObject {
    [Register ("onAdd", "([III)V",
            "GetOnAddHandler:Mono.Samples.SanityTests.IAdderProgressInvoker, SanityTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
    void OnAdd (JavaArray<int> values, int currentIndex, int currentSum);
}

請注意,在上述專案中,我們會將 Java int[] 參數對應至 JavaArray<int>。 這並非必要:我們可以將它系結至 C# int[]IList<int>或 完全的其他專案。 無論選擇何種類型, Invoker 都必須能夠將它轉譯為Java int[] 類型以進行調用。

Invoker 定義

類型 Invoker 定義必須繼承 Java.Lang.Object、實作適當的介面,並提供介面定義中參考的所有連接方法。 還有一個與類別系結不同的建議: class_ref 字段和方法標識符應該是實例成員,而不是靜態成員。

偏好實例成員的原因與 Android 運行時間的行為有關 JNIEnv.GetMethodID 。 (這可能是 Java 行為;它尚未經過測試。 JNIEnv.GetMethodID 查閱來自已實作介面而非宣告介面的方法時,會傳回 null。 請考慮 java.util.SortedMap<K、V> Java 介面,其會實作 java.util.Map<K、V> 介面。 Map 提供 明確的 方法,因此 SortedMap 看似合理的 Invoker 定義會是:

// Fails at runtime. DO NOT FOLLOW
partial class ISortedMapInvoker : Java.Lang.Object, ISortedMap {
    static IntPtr class_ref = JNIEnv.FindClass ("java/util/SortedMap");
    static IntPtr id_clear;
    public void Clear()
    {
        if (id_clear == IntPtr.Zero)
            id_clear = JNIEnv.GetMethodID(class_ref, "clear", "()V");
        JNIEnv.CallVoidMethod(Handle, id_clear);
    }
     // ...
}

此工作會失敗,因為JNIEnv.GetMethodID透過類別實例查閱 Map.clear 方法SortedMap時會傳回 null

有兩個解決方案:追蹤每個方法來自哪一 class_ref 個介面,並針對每個介面都有 ,或將所有專案保留為實例成員,並在最衍生類別類型上執行方法查閱,而不是介面類型。 後者是在 Mono.Android.dll完成的。

Invoker 定義有六個區段:建構函式、 Dispose 方法、 ThresholdTypeThresholdClass 成員、 GetObject 方法、介面方法實作,以及連接器方法實作。

建構函式

建構函式必須查閱所叫用之實例的運行時間類別,並將運行時間類別儲存在實例 class_ref 欄位中:

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

注意:屬性 Handle 必須在建構函式主體內使用,而不是 handle 參數,如同在Android v4.0上,在 handle 基底建構函式完成執行之後,參數可能無效。

Dispose 方法

方法 Dispose 必須釋放建構函式中配置的全域參考:

partial class IAdderProgressInvoker {
    protected override void Dispose (bool disposing)
    {
        if (this.class_ref != IntPtr.Zero)
            JNIEnv.DeleteGlobalRef (this.class_ref);
        this.class_ref = IntPtr.Zero;
        base.Dispose (disposing);
    }
}

ThresholdType 和 ThresholdClass

ThresholdTypeThresholdClass 成員與類別系結中找到的內容相同:

partial class IAdderProgressInvoker {
    protected override Type ThresholdType {
        get {
            return typeof (IAdderProgressInvoker);
        }
    }
    protected override IntPtr ThresholdClass {
        get {
            return class_ref;
        }
    }
}

GetObject 方法

需要靜態 GetObject 方法才能支援 Extensions.JavaCast<T>()

partial class IAdderProgressInvoker {
    public static IAdderProgress GetObject (IntPtr handle, JniHandleOwnership transfer)
    {
        return new IAdderProgressInvoker (handle, transfer);
    }
}

介面方法

介面的每個方法都需要實作,以透過 JNI 叫用對應的 Java 方法:

partial class IAdderProgressInvoker {
    IntPtr id_onAdd;
    public void OnAdd (JavaArray<int> values, int currentIndex, int currentSum)
    {
        if (id_onAdd == IntPtr.Zero)
            id_onAdd = JNIEnv.GetMethodID (class_ref, "onAdd", "([III)V");
        JNIEnv.CallVoidMethod (Handle, id_onAdd, new JValue (JNIEnv.ToJniHandle (values)), new JValue (currentIndex), new JValue (currentSum));
    }
}

連線 or 方法

連接器方法和支援基礎結構負責將 JNI 參數封送處理至適當的 C# 類型。 Java int[] 參數會以 JNI jintArray的形式傳遞,也就是 IntPtr C# 內的 。 IntPtr必須封送處理至 JavaArray<int> ,才能支援叫用 C# 介面:

partial class IAdderProgressInvoker {
    static Delegate cb_onAdd;
    static Delegate GetOnAddHandler ()
    {
        if (cb_onAdd == null)
            cb_onAdd = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr, IntPtr, int, int>) n_OnAdd);
        return cb_onAdd;
    }

    static void n_OnAdd (IntPtr jnienv, IntPtr lrefThis, IntPtr values, int currentIndex, int currentSum)
    {
        IAdderProgress __this = Java.Lang.Object.GetObject<IAdderProgress>(lrefThis, JniHandleOwnership.DoNotTransfer);
        using (var _values = new JavaArray<int>(values, JniHandleOwnership.DoNotTransfer)) {
            __this.OnAdd (_values, currentIndex, currentSum);
        }
    }
}

如果 int[] 偏好使用 JavaList<int>則可以改用 JNIEnv.GetArray()

int[] _values = (int[]) JNIEnv.GetArray(values, JniHandleOwnership.DoNotTransfer, typeof (int));

不過請注意,這會 JNIEnv.GetArray 在 VM 之間複製整個陣列,因此對於大型數位,這可能會導致大量新增的 GC 壓力。

完整叫用者定義

完整的 IAdderProgressInvoker 定義

class IAdderProgressInvoker : Java.Lang.Object, IAdderProgress {

    IntPtr class_ref;

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

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

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

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

    public static IAdderProgress GetObject (IntPtr handle, JniHandleOwnership transfer)
    {
        return new IAdderProgressInvoker (handle, transfer);
    }

#region OnAdd
    IntPtr id_onAdd;
    public void OnAdd (JavaArray<int> values, int currentIndex, int currentSum)
    {
        if (id_onAdd == IntPtr.Zero)
            id_onAdd = JNIEnv.GetMethodID (class_ref, "onAdd",
                    "([III)V");
        JNIEnv.CallVoidMethod (Handle, id_onAdd,
                new JValue (JNIEnv.ToJniHandle (values)),
                new JValue (currentIndex),
new JValue (currentSum));
    }

#pragma warning disable 0169
    static Delegate cb_onAdd;
    static Delegate GetOnAddHandler ()
    {
        if (cb_onAdd == null)
            cb_onAdd = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr, IntPtr, int, int>) n_OnAdd);
        return cb_onAdd;
    }

    static void n_OnAdd (IntPtr jnienv, IntPtr lrefThis, IntPtr values, int currentIndex, int currentSum)
    {
        IAdderProgress __this = Java.Lang.Object.GetObject<IAdderProgress>(lrefThis, JniHandleOwnership.DoNotTransfer);
        using (var _values = new JavaArray<int>(values, JniHandleOwnership.DoNotTransfer)) {
            __this.OnAdd (_values, currentIndex, currentSum);
        }
    }
#pragma warning restore 0169
#endregion
}

JNI 對象參考

許多 JNIEnv 方法都會傳回 JNI物件參考,類似於 GCHandles。 JNI 提供三種不同類型的對象參考:本機參考、全域參考和弱式全域參考。 這三個都表示為 System.IntPtr (根據 JNI 函數類型區段),並非所有 IntPtrJNIEnv 方法傳回的 都是參考。 例如, JNIEnv.GetMethodID 會傳回 IntPtr,但不會傳回對象參考,它會傳 jmethodID回 。 如需詳細資訊, 請參閱 JNI 函式檔

本機參考是由 大部分 的參考建立方法所建立。 Android 只允許在任何指定時間有有限的本機參考存在,通常是 512。 本機參考可以透過 JNIEnv.DeleteLocalRef 刪除。 不同於 JNI,並非所有傳回對象參考的參考 JNIEnv 方法都會傳回本機參考; JNIEnv.FindClass 會傳 回全域 參考。 強烈建議您盡可能快速地刪除本機參考,可能是藉由在 對象周圍建構 Java.Lang.Object,並指定JniHandleOwnership.TransferLocalRefJava.Lang.Object(IntPtr handle,JniHandleOwnership transfer) 建構函式。

全域參考是由 JNIEnv.NewGlobalRefJNIEnv.FindClass 所建立。 它們可以使用 JNIEnv.DeleteGlobalRef 終結。 模擬器的限制為 2,000 個未處理的全域參考,而硬體裝置的限制約為 52,000 個全域參考。

弱式全球參考僅適用於 Android v2.2 (Froyo) 和更新版本。 您可以使用 JNIEnv.DeleteWeakGlobalRef 刪除弱式全域參考。

處理 JNI 本機參考

JNIEnv.GetObjectField、JNIEnv.GetStaticObjectFieldJNIEnv.CallObjectMethod、JNIEnv.CallNonvirtualObjectMethodJNIEnv.CallStaticObjectMethod 方法會傳回IntPtr包含 Java 物件的 JNI 本機參考,如果 Java 傳null回 ,則IntPtr.Zero傳回 。 由於一次無法完成的本機參考數目有限(512 個專案),因此最好確保及時刪除參考。 有三種方式可以處理本機參考:明確刪除它們、建立 Java.Lang.Object 實例來保存它們,以及使用 Java.Lang.Object.GetObject<T>() 來建立其周圍的Managed可呼叫包裝函式。

明確刪除本機參考

JNIEnv.DeleteLocalRef 可用來刪除本機參考。 一旦刪除本機參考之後,就無法再使用它,因此請務必小心,以確保 JNIEnv.DeleteLocalRef 是使用本機參考完成的最後一件事。

IntPtr lref = JNIEnv.CallObjectMethod(instance, methodID);
try {
    // Do something with `lref`
}
finally {
    JNIEnv.DeleteLocalRef (lref);
}

使用 Java.Lang.Object 包裝

Java.Lang.Object提供 Java.Lang.Object(IntPtr handle, JniHandleOwnership transfer) 建構函式,可用來包裝結束的 JNI 參考。 JniHandleOwnership 參數會決定應如何處理IntPtr參數:

  • JniHandleOwnership.DoNotTransfer – 建立 Java.Lang.Object 的實例會從 handle 參數建立新的全域參考,而且 handle 不會變更。 呼叫端會在必要時負責釋放 handle

  • JniHandleOwnership.TransferLocalRef – 建立的Java.Lang.Object實例會從 handle 參數建立新的全域參考,並使用 handle JNIEnv.DeleteLocalRef 刪除。 呼叫端不得釋放 handle ,而且在建構函式完成執行之後不得使用 handle

  • JniHandleOwnership.TransferGlobalRef – 建立 Java.Lang.Object 的實例將接管參數的 handle 擁有權。 通話端不得釋放 handle

由於 JNI 方法調用方法會傳回本機 refs, JniHandleOwnership.TransferLocalRef 因此通常會使用:

IntPtr lref = JNIEnv.CallObjectMethod(instance, methodID);
var value = new Java.Lang.Object (lref, JniHandleOwnership.TransferLocalRef);

在垃圾收集實例之前 Java.Lang.Object ,將不會釋放建立的全域參考。 如果能夠,處置 實例將會釋放全域參考,以加速垃圾收集:

IntPtr lref = JNIEnv.CallObjectMethod(instance, methodID);
using (var value = new Java.Lang.Object (lref, JniHandleOwnership.TransferLocalRef)) {
    // use value ...
}

使用 Java.Lang.Object.GetObject<T>()

Java.Lang.Object提供 Java.Lang.Object.GetObject<T>(IntPtr handle, JniHandleOwnership transfer) 方法,可用來建立指定類型的 Managed 可呼叫包裝函式。

此類型 T 必須符合下列需求:

  1. T 必須是參考型別。

  2. T 必須實作 IJavaObject 介面。

  3. 如果 T 不是抽象類或介面,則必須 T 提供具有參數類型的 (IntPtr, JniHandleOwnership) 建構函式。

  4. 如果 T 是抽象類或介面,則必須可用的叫用程式T 叫用程式是繼承 T 或實 T 作 的非抽象型別,且名稱與 T Invoker 後綴相同。 例如,如果 T 是 介面 Java.Lang.IRunnable ,則類型 Java.Lang.IRunnableInvoker 必須存在,而且必須包含必要的 (IntPtr, JniHandleOwnership) 建構函式。

由於 JNI 方法調用方法會傳回本機 refs, JniHandleOwnership.TransferLocalRef 因此通常會使用:

IntPtr lrefString = JNIEnv.CallObjectMethod(instance, methodID);
Java.Lang.String value = Java.Lang.Object.GetObject<Java.Lang.String>( lrefString, JniHandleOwnership.TransferLocalRef);

查閱 Java 類型

若要查閱 JNI 中的欄位或方法,必須先查閱欄位或方法的宣告類型。 Android.Runtime.JNIEnv.FindClass(string)) 方法可用來查閱 Java 類型。 字串參數是 簡化的類型參考Java 類型的完整型別參考如需簡化和完整類型參考的詳細資訊,請參閱 JNI 類型參考一節

注意:不同於其他 JNIEnv 傳回物件實例的方法, FindClass 會傳回全域參考,而不是本機參考。

實例欄位

欄位是透過 欄位識別碼來操作。 欄位識別碼是透過 JNIEnv.GetFieldID 取得,這需要定義欄位的類別、功能變數名稱和 欄位的 JNI 類型簽章

欄位識別碼不需要釋放,只要載入對應的Java類型,欄位標識碼就有效。 (Android 目前不支援類別卸除。)

操作實例欄位的方法有兩組:一組用於讀取實例欄位,另一組用於寫入實例字段。 所有方法集都需要欄位識別元才能讀取或寫入域值。

讀取實例域值

讀取實體域值的方法集合遵循命名模式:

* JNIEnv.Get*Field(IntPtr instance, IntPtr fieldID);

其中 * 是欄位的類型:

寫入實例域值

撰寫實體域值的方法集合遵循命名模式:

JNIEnv.SetField(IntPtr instance, IntPtr fieldID, Type value);

其中 Type 是欄位的類型:

  • JNIEnv.SetField) – 寫入任何不是內建類型的欄位值,例如 java.lang.Object 、陣列和介面類型。 此值 IntPtr 可以是 JNI 區域參考、JNI 全域參考、JNI 弱式全域參考或 IntPtr.Zero (for null )。

  • JNIEnv.SetField) – 寫入實例欄位的值 bool

  • JNIEnv.SetField) – 寫入實例欄位的值 sbyte

  • JNIEnv.SetField) – 寫入實例欄位的值 char

  • JNIEnv.SetField) – 寫入實例欄位的值 short

  • JNIEnv.SetField) – 寫入實例欄位的值 int

  • JNIEnv.SetField) – 寫入實例欄位的值 long

  • JNIEnv.SetField) – 寫入實例欄位的值 float

  • JNIEnv.SetField) – 寫入實例欄位的值 double

靜態欄位

靜態欄位是透過 欄位標識碼來操作。 欄位識別碼是透過 JNIEnv.GetStaticFieldID 取得,其需要定義欄位的類別、功能變數名稱和 欄位的 JNI 類型簽章

欄位識別碼不需要釋放,只要載入對應的Java類型,欄位標識碼就有效。 (Android 目前不支援類別卸除。)

操作靜態欄位的方法有兩組:一組用於讀取實例欄位,另一組用於寫入實例字段。 所有方法集都需要欄位識別元才能讀取或寫入域值。

讀取靜態域值

讀取靜態域值的方法集合遵循命名模式:

* JNIEnv.GetStatic*Field(IntPtr class, IntPtr fieldID);

其中 * 是欄位的類型:

撰寫靜態域值

撰寫靜態域值的方法集合遵循命名模式:

JNIEnv.SetStaticField(IntPtr class, IntPtr fieldID, Type value);

其中 Type 是欄位的類型:

實例方法

實例方法是透過方法標識碼叫用。 方法標識碼是透過 JNIEnv.GetMethodID 取得,這需要定義方法的類型、方法的名稱,以及 方法的 JNI 類型簽章

方法標識碼不需要釋放,只要載入對應的Java類型,方法標識碼就有效。 (Android 目前不支援類別卸除。)

叫用方法有兩組方法:一組用於虛擬叫用方法,另一組用於非虛擬叫用方法。 這兩組方法都需要方法標識符來叫用 方法,而且非虛擬調用也需要指定應該叫用哪一個類別實作。

介面方法只能在宣告類型內查閱;無法查閱來自擴充/繼承介面的方法。 如需詳細資訊,請參閱稍後的系結介面/叫用程序實作一節。

可以查閱類別或任何基類或實作介面中宣告的任何方法。

虛擬方法調用

叫用方法的一組方法幾乎遵循命名模式:

* JNIEnv.Call*Method( IntPtr instance, IntPtr methodID, params JValue[] args );

其中 * 是方法的傳回型別。

非虛擬方法調用

叫用非虛擬方法的方法集合會遵循命名模式:

* JNIEnv.CallNonvirtual*Method( IntPtr instance, IntPtr class, IntPtr methodID, params JValue[] args );

其中 * 是方法的傳回型別。 非虛擬方法調用通常用來叫用虛擬方法的基底方法。

靜態方法

靜態方法是透過 方法標識碼叫用。 方法標識碼是透過 JNIEnv.GetStaticMethodID 取得,這需要定義方法的類型、方法的名稱,以及 方法的 JNI 類型簽章

方法標識碼不需要釋放,只要載入對應的Java類型,方法標識碼就有效。 (Android 目前不支援類別卸除。)

靜態方法調用

叫用方法的一組方法幾乎遵循命名模式:

* JNIEnv.CallStatic*Method( IntPtr class, IntPtr methodID, params JValue[] args );

其中 * 是方法的傳回型別。

JNI 類型簽章

JNI 類型簽章JNI 類型參考 (雖然不是簡化的類型參考),但方法除外。 使用 方法時,JNI 類型簽章是一個開放式括弧 '(',後面接著串連所有參數類型的類型參考(不含分隔逗號或其他任何專案),後面接著右括弧 ')',後面接著方法傳回類型的 JNI 類型參考。

例如,假設 Java 方法:

long f(int n, String s, int[] array);

JNI 類型簽章會是:

(ILjava/lang/String;[I)J

一般而言, 強烈建議 使用 javap 命令來判斷 JNI 簽章。 例如,java.lang.Thread.State.valueOf(String) 方法的 JNI 類型簽章是 “(Ljava/lang/String;)Ljava/lang/Thread$State;“,而 java.lang.Thread.State.values 方法的 JNI 類型簽章為 ”()[Ljava/lang/Thread$State;“。 注意尾端分號;這些 JNI 類型簽章的一部分。

JNI 類型參考

JNI 類型參考與 Java 類型參考不同。 您無法使用完整 Java 類型名稱,例如 java.lang.String 搭配 JNI,您必須改用 JNI 變化 "java/lang/String""Ljava/lang/String;",視內容而定;如需詳細資訊,請參閱下方。 JNI 類型參考有四種類型:

  • 內建
  • 簡化
  • type
  • array

內建類型參考

內建類型參考是單一字元,用來參考內建實值型別。 對應如下所示:

  • "B"sbyte
  • "S"short
  • "I"int
  • "J"long
  • "F"float
  • "D"double
  • "C"char
  • "Z"bool
  • "V" for void 方法傳回型別。

簡化的類型參考

簡化的類型參考只能在 JNIEnv.FindClass(string)中使用。 有兩種方式可以衍生簡化的類型參考:

  1. 從完整 Java 名稱中,將套件名稱內的每一個 '.' 取代為 ,並將類型名稱 '/' 前面的每一個 取代為 ,並將類型名稱內的每 '.' 一個取代為 '$'

  2. 讀取的 'unzip -l android.jar | grep JavaName' 輸出。

這兩者之一會導致 Java 類型 java.lang.Thread.State 對應至簡化的類型參考 java/lang/Thread$State

類型參考

類型參考是內建類型參考,或是具有 'L' 前置詞和 ';' 後綴的簡化型別參考。 針對 Java 類型 java.lang.String,簡化的類型參考為 "java/lang/String",而類型參考為 "Ljava/lang/String;"

類型參考會與數位類型參考和 JNI 簽章搭配使用。

若要取得型別參考,另一個方法是讀取 的 'javap -s -classpath android.jar fully.qualified.Java.Name'輸出。 視所涉及的類型而定,您可以使用建構函式宣告或方法傳回類型來判斷 JNI 名稱。 例如:

$ javap -classpath android.jar -s java.lang.Thread.State
Compiled from "Thread.java"
public final class java.lang.Thread$State extends java.lang.Enum{
public static final java.lang.Thread$State NEW;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State RUNNABLE;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State BLOCKED;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State WAITING;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State TIMED_WAITING;
  Signature: Ljava/lang/Thread$State;
public static final java.lang.Thread$State TERMINATED;
  Signature: Ljava/lang/Thread$State;
public static java.lang.Thread$State[] values();
  Signature: ()[Ljava/lang/Thread$State;
public static java.lang.Thread$State valueOf(java.lang.String);
  Signature: (Ljava/lang/String;)Ljava/lang/Thread$State;
static {};
  Signature: ()V
}

Thread.State 是 Java 列舉類型,因此我們可以使用 方法的 valueOf Signature 來判斷類型參考為 Ljava/lang/Thread$State;。

陣列類型參考

數位型別參考前面會 '[' 加上 JNI 類型參考。 指定陣列時,無法使用簡化的類型參考。

例如, int[]"[I"int[][]"[[I", 是 , 是 java.lang.Object[]"[Ljava/lang/Object;"

Java 泛型和類型清除

大部分 時間,如透過 JNI 所見,Java 泛型 不存在。 有一些「皺紋」,但這些皺紋是在 Java 如何與泛型互動,而不是 JNI 查閱和叫用泛型成員的方式。

透過 JNI 互動時,泛型型別或成員與非泛型型別或成員之間沒有任何差異。 例如,泛型型 別 java.lang.Class<T> 也是「原始」泛型型 java.lang.Class別,兩者都有相同的簡化型別參考 "java/lang/Class"

Java 原生介面支援

Android.Runtime.JNIEnv 是 Jave 原生介面 (JNI) 的 Managed 包裝函式。 JNI 函式是在 Java 原生介面規格宣告,不過方法已變更以移除明確JNIEnv*參數,而且IntPtr會使用 ,而不是 jobjectjclassjmethodID等。例如,請考慮 JNI NewObject 函式

jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);

這會公開為 JNIEnv.NewObject 方法:

public static IntPtr NewObject(IntPtr clazz, IntPtr jmethod, params JValue[] parms);

在兩個呼叫之間翻譯相當簡單。 在 C 中,您會有:

jobject CreateMapActivity(JNIEnv *env)
{
    jclass    Map_Class   = (*env)->FindClass(env, "mono/samples/googlemaps/MyMapActivity");
    jmethodID Map_defCtor = (*env)->GetMethodID (env, Map_Class, "<init>", "()V");
    jobject   instance    = (*env)->NewObject (env, Map_Class, Map_defCtor);

    return instance;
}

C# 對等專案如下:

IntPtr CreateMapActivity()
{
    IntPtr Map_Class   = JNIEnv.FindClass ("mono/samples/googlemaps/MyMapActivity");
    IntPtr Map_defCtor = JNIEnv.GetMethodID (Map_Class, "<init>", "()V");
    IntPtr instance    = JNIEnv.NewObject (Map_Class, Map_defCtor);

    return instance;
}

一旦您在 IntPtr 中保留 Java 物件實例,您可能想要使用它執行一些動作。 您可以使用 JNIEnv.CallVoidMethod() 之類的 JNIEnv 方法來執行此動作,但如果已經有類似 C# 包裝函式,則您會想要透過 JNI 參考建構包裝函式。 您可以透過 Extensions.JavaCast<T> 擴充方法來執行此動作:

IntPtr lrefActivity = CreateMapActivity();

// imagine that Activity were instead an interface or abstract type...
Activity mapActivity = new Java.Lang.Object(lrefActivity, JniHandleOwnership.TransferLocalRef)
    .JavaCast<Activity>();

您也可以使用 Java.Lang.Object.GetObject<T> 方法:

IntPtr lrefActivity = CreateMapActivity();

// imagine that Activity were instead an interface or abstract type...
Activity mapActivity = Java.Lang.Object.GetObject<Activity>(lrefActivity, JniHandleOwnership.TransferLocalRef);

此外,移除每個 JNI 函式中存在的參數,即可修改 JNIEnv* 所有 JNI 函式。

摘要

直接處理 JNI 是一種可怕的經歷,應該不費一切代價避免。 不幸的是,它並不總是可以避免的;希望本指南會在您使用 Android 版 Mono 達到未系結的 Java 案例時提供一些協助。