JNI 提示

JNI 是 Java 原生介面。它定義了 Android 從受控程式碼 (以 Java 或 Kotlin 程式設計語言編寫) 編譯的位元碼,與原生程式碼 (以 C/C++ 編寫) 互動的一種方式。JNI 是供應商中立的,可支援從動態共用程式庫載入程式碼,雖然有時會造成不便,但相當有效率。

注意:由於 Android 會以與 Java 程式設計語言類似的方式,將 Kotlin 編譯為 ART 友善位元碼,因此您可以將本頁的指南套用至 JNI 架構和相關成本的 Kotlin 和 Java 程式設計語言。詳情請參閱: Kotlin 和 Android

如果您還不熟悉,請參閱 Java 原生介面規格 ,瞭解 JNI 的運作方式和可用功能。只有部分通知 無法在使用者眼前 閱讀,因此接下來的幾個章節就能派上用場。

如要瀏覽全域 JNI 參照,並查看全域 JNI 參照的建立和刪除位置,請使用 記憶體分析器中的「JNI heap」檢視畫面 Android Studio 3.2 以上版本。

常見提示

請盡量減少 JNI 層的足跡。這裡有幾個維度需要考量。您的 JNI 解決方案應嘗試遵循下列規範 (列於下方列出重要性排序、 從最重要的兩個開始):

  • 盡量減少 JNI 層的資源管理作業。共婚 JNI 層的成本並不高您設計的介面可以盡量減少 您需要整理資料,以及所需彙整資料的頻率。
  • 盡可能避免以受管理程式設計語言編寫的程式碼與以 C++ 編寫的程式碼之間的非同步通訊。這樣可以讓 JNI 介面更易於維護。一般來說,您可以讓非同步更新使用與 UI 相同的語言,藉此簡化非同步 UI 更新作業。舉例來說,請不要透過 JNI 從 Java 程式碼中的 UI 執行緒叫用 C++ 函式,而是在 Java 程式設計語言中,在兩個執行緒之間執行回呼,其中一個執行緒進行封鎖 C++ 呼叫,然後在封鎖呼叫完成時通知 UI 執行緒。
  • 盡量減少需要與 JNI 互動的執行緒數量。 如果您需要在 Java 和 C++ 兩種語言中使用執行緒集區,請盡量讓 JNI 在集區擁有者之間通訊,而非在個別背景工作執行緒之間通訊。
  • 請將介面程式碼放在少數幾個容易辨識的 C++ 和 Java 來源位置,方便日後重構。考慮使用 JNI 自動產生功能 程式庫。

JavaVM 和 JNIEnv

JNI 定義了兩種主要資料結構:「JavaVM」和「JNIEnv」這兩者基本上都是指向函式表的指標。(在 C++ 版本中,它們屬於 指向函式資料表的指標,以及間接透過 )。JavaVM 提供「叫用介面」函式,可讓您建立及刪除 JavaVM。理論上,每個程序可以有多個 JavaVM 但 Android 只允許一種

JNIEnv 提供大部分 JNI 函式。您的原生函式都會以下列形式接收 JNIEnv 第一個引數,@CriticalNative 方法除外 請參閱更快速的原生呼叫

JNIEnv 用於執行緒本機儲存空間。因此,您無法在執行緒之間共用 JNIEnv。如果某段程式碼無法透過其他方式取得 JNIEnv,您應分享 ,並使用 GetEnv 找出執行緒的 JNIEnv。(假設有一個這類 ID,請參閱下方的 AttachCurrentThread)。

JNIEnv 和 JavaVM 的 C 宣告與 C++ 宣告不同。"jni.h" 包含檔案會提供不同的 typedef,這取決於是否已納入 C 或 C++。因此,在包含這兩種語言的標頭檔案中納入 JNIEnv 引數是不好的做法。(換個角度來看,如果 標頭檔案必須包含 #ifdef __cplusplus,如果 。

執行緒

所有執行緒都是 Linux 執行緒,由核心排定。通常 從受管理的程式碼開始 (使用 Thread.start()), 但您也可以先在其他位置建立這些物件,然後附加至 JavaVM。適用對象 例如以 pthread_create()std::thread 開頭的執行緒 可以使用 AttachCurrentThread()AttachCurrentThreadAsDaemon() 函式。直到討論串結束為止 沒有 JNIEnv,並且無法發出 JNI 呼叫

通常建議使用 Thread.start() 建立任何需要呼叫 Java 程式碼的執行緒。這樣可確保有足夠的堆疊空間 正確的 ThreadGroup,而且您使用相同的 ClassLoader 做為 Java 程式碼相較於 原生程式碼 (如果您使用 pthread_t,請參閱 pthread_setname_np()thread_tstd::thread::native_handle() (如果有 std::thread,且想要 pthread_t)。

附加原生建立的執行緒會導致 java.lang.Thread 物件建構並新增至「main」ThreadGroup,讓偵錯工具能夠查看該物件。正在撥打 AttachCurrentThread() 附加執行緒上的操作是免人工管理。

Android 不會暫停執行原生程式碼的執行緒。如果垃圾收集程序正在進行,或偵錯工具已發出暫停要求,Android 會在下次進行 JNI 呼叫時暫停執行緒。

透過 JNI 連接的執行緒必須呼叫 離開DetachCurrentThread()。 如果直接編寫程式碼時遇到問題,在 Android 2.0 (Eclair) 以上版本中 可以使用 pthread_key_create() 定義解構函式 執行緒結束時會呼叫的函式;以及 然後從位置呼叫 DetachCurrentThread()(使用這些方法) 金鑰和 pthread_setspecific(),用於儲存 JNIEnv 執行緒本機儲存空間;然後經由系統傳送 引數)。

jclass、jmethodID 和 jfieldID

如要從原生程式碼存取物件的欄位,請執行下列步驟:

  • 使用 FindClass 取得類別的類別物件參照
  • 使用 GetFieldID 取得欄位 ID
  • 取得適當的欄位內容,例如 GetIntField

同樣地,如要呼叫方法,您必須先取得類別物件參照,然後再取得方法 ID。這些 ID 通常只是指向內部執行階段資料結構的指標。查詢可能需要多個字串 但在您獲得實際呼叫以取得欄位或叫用方法後 執行速度非常快

如果效能很重要,建議您一次查看值並快取結果 。由於每個程序的 JavaVM 數量上限為 1,因此將這類資料儲存在靜態本機結構中是合理的做法。

在卸載類別之前,類別參照、欄位 ID 和方法 ID 仍然有效。只有在與 ClassLoader 相關聯的所有類別都能進行垃圾收集時,才會卸載類別,這在 Android 中很少見,但並非不可能。不過請注意 jclass 為類別參照,且必須受到呼叫保護NewGlobalRef (請參閱下一節)。

如果您想要在載入類別時快取 ID,並自動重新快取這些 ID 如果類別已卸載並重新載入,正確的初始化方式 ID 就是將程式碼片段加入適當的類別,如下所示:

Kotlin

companion object {
    /*
     * We use a static class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private external fun nativeInit()

    init {
        nativeInit()
    }
}

Java

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

在執行 ID 查詢的 C/C++ 程式碼中建立 nativeClassInit 方法。程式碼 只會在類別初始化時執行一次。如果類別曾經卸載,然後重新載入,則會再次執行。

地方和全球參考資料

傳遞至原生方法的每個引數,以及 JNI 函式傳回的幾乎所有物件,都是「本機參照」。這表示該聯播網的 目前原生方法在目前執行緒內持續運作的時間。 即使物件本身在原生方法之後持續運作 會傳回,表示參照無效。

這適用於 jobject 的所有子類別,包括 jclassjstringjarray。 (擴充 JNI 時,執行階段會針對大部分參照錯誤情況發出警告 檢查功能是否已啟用)。

如要取得非本機參照,只能使用函式 《NewGlobalRef》和《NewWeakGlobalRef》。

如果您想將參照保留更久的時間,必須使用「全域」參照。NewGlobalRef 函式會採用 做為引數並傳回全域參照。 在您呼叫 DeleteGlobalRef 之前,全域參照項目保證有效。

這種模式常用於快取傳回的 jclass 來自 FindClass,例如:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

所有 JNI 方法都會接受局部和全域參照做為引數。參照相同物件時,可能會有不同的值。 舉例來說,在同一個物件上連續呼叫 NewGlobalRef 的傳回值可能會有所不同。如要查看兩個參照是否參照同一個物件, 您必須使用 IsSameObject 函式。請勿在原生程式碼中將參照項目與 == 進行比較。

這種情況會產生一個後果,就是您 不得假設物件參照是常數或不重複 原生程式碼代表物件的值可能會不同 將一個方法叫用到下一個方法,有可能兩個 不同的物件在連續呼叫中可能有相同的值。不使用 jobject 值做為鍵。

程式設計師必須「未過度分配」本機參照。從實務上來說 如果要建立大量本機參照 您應使用 kubectl 指令 DeleteLocalRef,而不必讓 JNI 為您執行操作。 只有在實際工作環境中 16 個本機參照,因此如果需要額外的資料,請在刪除或使用 第 EnsureLocalCapacity 名 (共 PushLocalFrame 下) 即可保留更多儲存空間。

請注意,jfieldIDjmethodID 是不可辨識的類型,並非物件參照,因此不應傳遞至 NewGlobalRefGetStringUTFCharsGetByteArrayElements 等函式傳回的原始資料指標也不是物件。(可能會通過這些測試) ,且有效期限到相符的 Release 呼叫為止)。

有一個特殊案例值得特別提及。如果要將原生伺服器 與 AttachCurrentThread 的執行緒,則執行的程式碼 一律不會自動釋放本機參照,直到執行緒卸離為止。當地任何國家/地區 您建立的參考檔案必須手動刪除。一般來說,任何原生 在迴圈中建立本機參照的程式碼可能需手動執行 刪除。

使用全域參照時請務必小心。全域參照可能無法避免,但難以偵錯,且可能會導致難以診斷的記憶體 (錯誤) 行為。所有其他條件都相同時 提供的全域參照較少,最好

UTF-8 和 UTF-16 字串

Java 程式設計語言使用 UTF-16。為了方便起見,JNI 提供可以 已修改 UTF-8。經過修改的編碼對 C 程式碼很有幫助,因為它會將 \u0000 編碼為 0xc0 0x80,而非 0x00。這麼做的好處是,您可以使用 C 樣式的零結尾字串,這類字串適合搭配標準 libc 字串函式使用。缺點是,您無法將任意 UTF-8 資料傳遞至 JNI,並期望其正常運作。

如要取得 String 的 UTF-16 表示法,請使用 GetStringChars。請注意,UTF-16 字串並非以零結尾,且允許使用「\u0000」。 因此,請等候字串長度以及 jchar 指標。

別忘了Release Get 的字串。字串函式會傳回 jchar*jbyte*,這是原始資料的 C 風格指標,而非本機參照。他們 保證在呼叫 Release 之前有效 會在原生方法傳回時釋出

傳遞至 NewStringUTF 的資料必須使用修改後的 UTF-8 格式。常見的錯誤是從檔案或網路串流讀取字元資料,然後將其交給 NewStringUTF,而未加以篩選。除非您知道資料是有效的 MUTF-8 (或 7 位元 ASCII,這是相容的子集) 您必須刪除無效字元,或將這些字元轉換為正確的修改 UTF-8 表單。 否則採用 UTF-16 轉換,可能會產生非預期的結果。 CheckJNI 是模擬器預設啟用的功能,可掃描字串,並在收到無效輸入內容時終止 VM。

在 Android 8 之前,使用 UTF-16 字串通常會比較快,因為 Android 不需要在 GetStringChars 中複製,而 GetStringUTFChars 需要分配並轉換為 UTF-8。Android 8 已變更 String 表示法,使其為每個字元使用 8 位元 ,並已開始使用 移動中 垃圾收集器這些功能大幅減少 無須複製資料,即可提供 String 資料的指標, (GetStringCritical)。但如果程式碼處理大部分字串 偏短,在大多數情況下 使用堆疊分配的緩衝區和 GetStringRegion,或者 GetStringUTFRegion。例如:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr<jchar[]> heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

原始陣列

JNI 提供可存取陣列物件內容的函式。雖然物件陣列必須一次存取一個項目,但 原始元可供讀取及寫入,就像在 C 中宣告一樣。

為了讓介面盡可能提高效率,且不限制 VM 實作,Get<PrimitiveType>ArrayElements 系列呼叫可讓執行階段傳回實際元素的指標,或分配一些記憶體並製作副本。無論如何,在發出對應的 Release 呼叫之前,系統都保證會傳回有效的原始指標 (這表示如果資料未複製,陣列物件就會固定,且無法在堆積壓縮期間重新配置)。您必須在 Get 的每個陣列中 Release此外,如果 Get 您必須確保程式碼不會嘗試對空值進行 Release 指標。

如果您想確認資料是否已複製,只要傳入 isCopy 引數的非 NULL 指標。但這種情況很少見 很實用

Release 呼叫會採用 mode 引數,這個引數可 就會有三個值執行階段執行的動作取決於 它會傳回指向實際資料或資料副本的指標:

  • 0
    • 實際:陣列物件未固定。
    • 文案:資料已複製回。含有副本的緩衝區已釋出。
  • JNI_COMMIT
    • 實際:什麼都不做。
    • 文案:資料已複製回。包含副本的緩衝區
  • JNI_ABORT
    • 實際:陣列物件未固定。較早 寫入作業不會中止。
    • 文案:有複製的緩衝區已釋出;任何變更內容都會遺失

檢查 isCopy 旗標的其中一個原因是 你必須透過 JNI_COMMIT 呼叫 Release 變更陣列後 - 如要在變更陣列和元素之間變更 來變更和執行使用陣列內容的程式碼 將 我們會略過免人工管理修訂的修訂版本另一個檢查標記的原因可能是 以高效率處理 JNI_ABORT。舉例來說 來取得陣列、在定位修改該陣列、將部分傳遞至其他函式,以及 然後捨棄變更如果您知道 JNI 會為 不必建立其他「可編輯」複製。如果 JNI 傳遞原始值,您就需要自行建立副本。

這很常見錯誤 (在範例程式碼中重複),假設您在下列情況中可以略過 Release 呼叫 *isCopy 為 false。但此聲明與事實不符。如果沒有複製緩衝區 然後原始記憶體必須固定,無法移動 我們現在要對垃圾收集器進行確認

另外請注意,JNI_COMMIT 標記「不會」釋出陣列, 您將需要以其他旗標再次呼叫 Release

區域呼叫

這是 Get<Type>ArrayElements 等呼叫的替代方案。 還有「GetStringChars」或許能夠派上用場 正確做法就是將資料複製到或匯出請把握以下幾項重點:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

這會擷取陣列,並複製前 len 個位元組 然後將陣列釋放。根據用途 實作後,Get 呼叫會釘選或複製陣列 內容。 程式碼會複製資料 (可能會複製第二次),然後呼叫 Release;在本例中,JNI_ABORT 會確保不會複製第三次。

一項功能更易於達成相同的目的:

    env->GetByteArrayRegion(array, 0, len, buffer);

這麼做有幾個優點:

  • 需要一次 (而非 2) 呼叫一次,可降低負擔。
  • 不需要綁定或額外的資料副本。
  • 降低程式設計錯誤的風險,避免在發生錯誤後忘記呼叫 Release

同樣地,您也可以使用 Set<Type>ArrayRegion 呼叫 將資料複製到陣列中,且 GetStringRegionGetStringUTFRegion 會將字元從 String

例外狀況

在例外狀況待處理期間,您不得呼叫大部分的 JNI 函式。 程式碼應會注意到例外狀況 (透過函式的傳回值 ExceptionCheckExceptionOccurred) 並傳回,或清除例外狀況並加以處理。

在例外狀況待處理期間,您只能呼叫下列 JNI 函式:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

許多 JNI 呼叫都可能擲回例外狀況,但通常會提供更簡單的方式來檢查失敗情形。舉例來說,如果 NewString 傳回 非空值,您不需要查看例外狀況。不過, 您呼叫方法 (使用 CallObjectMethod 等函式) 因為傳回值 才有效。

請注意,代管程式碼擲回的例外狀況不會解開原生堆疊 相輔相成。(此外,一般不建議在 Android 上使用 C++ 例外狀況, 從 C++ 程式碼傳遞至代管程式碼的 JNI 轉換邊界。) JNI ThrowThrowNew 指令 在目前的執行緒中設定例外狀況指標。從原生程式碼返回至 Managed 時,系統會記錄並適當處理例外狀況。

原生程式碼可能會「catch」例外狀況,方法是呼叫 ExceptionCheck;或 ExceptionOccurred,並以下列方式清除: ExceptionClear。和往常一樣 如果不處理就捨棄例外狀況,可能會造成問題。

沒有內建函式可操控 Throwable 物件 因此如果您要 (假設) 取得例外狀況字串 找出 Throwable 類別,並查詢 getMessage "()Ljava/lang/String;",請叫用此函式,如果結果 不是空值,請使用 GetStringUTFChars 來取得 將其交給 printf(3) 或同等單位。

延伸檢查

JNI 幾乎不需要檢查錯誤。錯誤通常會導致應用程式當機。Android 也提供名為 CheckJNI 的模式,其中 JavaVM 和 JNIEnv 函式表指標會切換至函式表,這些函式會在呼叫標準實作項目之前執行一系列延伸檢查。

其他檢查項目包括:

  • 陣列:嘗試分配負大小的陣列。
  • 錯誤的指標:將錯誤的 jarray/jclass/jobject/jstring 傳遞至 JNI 呼叫,或是將空值指標傳遞至具有非空值引數的 JNI 呼叫。
  • 類別名稱:將「java/lang/String」樣式的類別名稱以外的任何內容傳遞至 JNI 呼叫。
  • 重要呼叫:在「重要」get 和對應的釋放作業之間進行 JNI 呼叫。
  • 直接 ByteBuffers:將錯誤的引數傳遞至 NewDirectByteBuffer
  • 例外狀況:在有待處理例外狀況的情況下發出 JNI 呼叫。
  • JNIEnv*s:使用錯誤的執行緒中的 JNIEnv*。
  • jfieldID:使用 NULL jfieldID,或使用 jfieldID 將欄位設為錯誤類型的值 (嘗試將 StringBuilder 指派給 String 欄位,例如,或針對靜態欄位使用 jfieldID) 來設定執行個體欄位,反之亦然,或使用不同類別例項的 jfieldID 等類別。
  • jmethodID:在發出 Call*Method JNI 呼叫時使用錯誤的 jmethodID:傳回類型不正確、靜態/非靜態不相符、「this」類型有誤 (適用於非靜態呼叫) 或錯誤的類別 (用於靜態呼叫)。
  • 參考檔案:在錯誤的參照類型上使用 DeleteGlobalRef/DeleteLocalRef
  • 版本模式:將錯誤的發布模式傳遞至發布呼叫 (0JNI_ABORTJNI_COMMIT 除外)。
  • 類型安全性:從原生方法傳回不相容的類型 (例如,從宣告為傳回字串的方法傳回 StringBuilder)。
  • UTF-8:將無效的修改 UTF-8 位元組序列傳送至 JNI 呼叫。

(系統不會檢查方法和欄位的無障礙程度:存取限制不適用於原生程式碼)。

您可以透過多種方式啟用 CheckJNI。

如果您使用的是模擬器,系統會預設開啟 CheckJNI。

如果您有已解鎖裝置,可以在啟用 CheckJNI 的情況下使用下列一系列指令重新啟動執行階段:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

無論是哪種情況,在執行階段啟動時,您都會在 logcat 輸出中看到類似以下的內容:

D AndroidRuntime: CheckJNI is ON

如果您使用一般裝置,可以使用下列指令:

adb shell setprop debug.checkjni 1

這不會影響已在執行中的應用程式,但從該時間點啟動的任何應用程式,都會啟用 CheckJNI。(將屬性變更為任何其他值或直接重新啟動,會再次停用 CheckJNI)。在這種情況下,下次應用程式啟動時,您會在 logcat 輸出中看到類似以下的內容:

D Late-enabling CheckJNI

您也可以將應用程式資訊清單中的 android:debuggable 屬性設為 為您的應用程式開啟 CheckJNI。請注意,Android 建構工具會自動 特定建構類型

原生程式庫

您可以使用標準的程式庫,載入共用程式庫中的原生程式碼 System.loadLibrary

實際上,舊版 Android 的 PackageManager 中有錯誤導致安裝, 請務必確保原生資料庫不穩定可靠ReLinker 專案提供瞭解決方法和其他原生程式庫載入問題的解決方法。

從靜態類別初始化器呼叫 System.loadLibrary (或 ReLinker.loadLibrary)。引數是「未經修飾」的程式庫名稱,因此如要載入 libfubar.so,您必須傳入 "fubar"

如果只有一個包含原生方法的類別,呼叫 System.loadLibrary 設定為該類別的靜態初始化器。否則,您可能會想從 Application 發出呼叫,以便瞭解程式庫是否一律載入,以及是否一律提早載入。

執行階段可以透過兩種方式尋找原生方法。您可以使用 RegisterNatives 明確註冊這些類別,也可以讓執行階段透過 dlsym 動態查詢這些類別。RegisterNatives 的優勢在於 檢查符號是否存在,您也不必 但會匯出 JNI_OnLoad 以外的任何資料。讓執行階段探索 函式是指可編寫的程式碼略少

如何使用「RegisterNatives」:

  • 提供 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 函式。
  • JNI_OnLoad 中,使用 RegisterNatives 註冊所有原生方法。
  • 請使用版本指令碼 (建議) 或 -fvisibility=hidden 進行建構,這樣只會從程式庫匯出 JNI_OnLoad。這可產生更快、更小的程式碼,並避免潛在的 會與應用程式載入的其他程式庫發生衝突,但可能會產生較不實用的堆疊追蹤 如果您的應用程式會在原生程式碼中異常終止)。

靜態初始化器應如下所示:

Kotlin

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Java

static {
    System.loadLibrary("fubar");
}

JNI_OnLoad 函式應如下所示: :

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

如要改用「探索」功能原生方法的命名方式,就必須以特定方式命名 (請參閱 JNI 規格 )。這表示如果方法簽章有誤,必須等到 即可。

JNI_OnLoad 發出的任何 FindClass 呼叫都會解析 用來載入共用資料庫的類別載入器結構定義。從其他情境呼叫時,FindClass 會使用與 Java 堆疊頂端方法相關聯的類別載入器,如果沒有這樣的類別載入器 (因為呼叫來自剛附加的原生執行緒),則會使用「系統」類別載入器。系統類別載入器無法得知應用程式的 類別,因此您將無法使用 FindClass 查詢自己的類別 相關資訊如此一來,JNI_OnLoad 就可讓您輕鬆查詢及快取類別: 具備有效的 jclass 全域參考資料 可以在任何附加的會話串中使用

使用 @FastNative@CriticalNative 加快原生呼叫的速度

您可以使用 @FastNative@CriticalNative 註解原生方法 (但不能同時使用兩者),加快受管理程式碼和原生程式碼之間的轉換速度。不過,這些註解會導致行為發生特定變化,因此使用前請務必仔細考量。雖然我們在下文中簡要提及這些變更,但請參閱說明文件以瞭解詳細資訊。

@CriticalNative 註解只能套用至不支援 使用代管物件 (在參數或傳回值中,或做為隱式 this),以及 註解會變更 JNI 轉換 ABI。原生實作必須從函式簽章中排除 JNIEnvjclass 參數。

執行 @FastNative@CriticalNative 方法時,垃圾 集合功能無法暫停重要工作的執行緒,而且可能會遭到封鎖。請勿使用 適用於長時間執行方法的註解,包括通常快速,但一般無限制的方法。 特別是,程式碼不應執行大量的 I/O 作業,或取得 可長時間保留一些活動

Android 8 起,這些註解已實作供系統使用,並在 Android 14 中成為經過 CTS 測試的公開 API。雖然這些最佳化功能可能也適用於 Android 8 到 13 裝置 (儘管 但原生方法的動態查詢功能僅支援 在 Android 12 以上版本中,必須使用 JNI RegisterNatives 明確註冊 可在 Android 8 到 11 版中執行Android 7 會忽略這些註解,因為 ABI 不相符 @CriticalNative 會導致發生引數處理錯誤,並有可能當機。

針對對效能至關重要的方法需要這些註解,強烈建議您 使用 JNI RegisterNatives 明確註冊方法,而不是依賴 以名稱為基礎的「探索」原生方法為獲得最佳應用程式啟動效能,建議您採用 納入 @FastNative@CriticalNative 方法的呼叫端 基準設定檔。從 Android 12 開始 對已編譯的代管方法呼叫 @CriticalNative 原生方法幾乎與 成本低廉,只要在 C/C++ 中用非內嵌呼叫,只要所有引數都可納入暫存器即可 (例如: arm64 上有 8 個完整性和最多 8 個浮點引數)。

有時候,最好將原生方法分割成 2 個, 另一個能處理緩慢案例例如:

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

64 位元注意事項

如要支援採用 64 位元指標的架構,請使用 long 欄位,而不是 將指標儲存至 Java 欄位內的原生結構指標時為 int

不支援的功能/回溯相容性

支援所有 JNI 1.6 功能,但以下情況除外:

  • 未實作 DefineClass。Android 不會 Java 位元碼或類別檔案,因此傳入二進位類別資料 無法運作。

為了提供與舊版 Android 的回溯相容性,您可能需要 請留意以下事項:

  • 原生函式的動態查詢功能

    在 Android 2.0 (Eclair) 版本之前,「$」字元不正確 轉換為「_00024」搜尋方法名稱如要解決這個問題,您必須使用明確註冊,或將原生方法移出內部類別。

  • 卸離執行緒

    在 Android 2.0 (Eclair) 之前,無法使用 pthread_key_create 析構函式來避免「執行緒必須在結束前解除連結」的檢查。(執行階段也會使用 pthread 鍵值解構函式,因此會發生競爭,看哪個函式會先呼叫)。

  • 弱全域參照

    在 Android 2.2 (Froyo) 之前,尚未實作弱式參照。 舊版本將會審慎拒絕使用。別擔心!您可以使用 Android 平台版本常數。

    在 Android 4.0 (Ice Cream Sandwich) 之前,較弱的全域參照只能 會傳遞至 NewLocalRefNewGlobalRefDeleteWeakGlobalRef。 程式設計師會在實際操作前,建立難以參照的弱勢全域 因此不應完全受限)。

    從 Android 4.0 (Ice Cream Sandwich) 開始,弱全域參照可像其他 JNI 參照一樣使用。

  • 當地參考資料

    搭載 Android 4.0 (Ice Cream Sandwich) 之前,本機參照 實際上Ice Cream Sandwich 將間接性 但這意味著 在舊版本中無法偵測出 JNI 錯誤。詳情請見 詳情請參閱 ICS 中的 JNI 本機參考資料變更

    Android 8.0 以下版本的 Android 中, 本機參照數量最多有版本限制。從 Android 8.0 開始 Android 支援不限數量的本機參照。

  • 使用 GetObjectRefType 判斷參照類型

    直到 Android 4.0 (Ice Cream Sandwich) 為止,由於使用過程的關係, 直接指標 (如上方所示) 就無法執行 正確GetObjectRefType。我們改用啟發式搜尋,依序檢查弱全域變數表、引數、區域變數表和全域變數表。初次發現您的 ,就會回報您的參照屬於其類型 未經過檢查舉例來說 您在以下國家/地區呼叫了 GetObjectRefType: 與傳遞做為隱含引數至靜態資料的 jclass 相同 您會取得 JNILocalRefType,而非 JNIGlobalRefType

  • @FastNative@CriticalNative

    在 Android 7 之前,系統會忽略這些最佳化註解。ABI 如果「@CriticalNative」不相符,會導致引數錯誤 也可能引發當機問題

    針對 @FastNative@CriticalNative 方法的本機函式動態查詢在 Android 8 至 10 中未實作,且在 Android 11 中含有已知的錯誤。在沒有設計的情況下使用這些最佳化 向 JNI RegisterNatives 明確註冊的可能 可能導致 Android 8-11 版本當機

  • FindClass 擲回 ClassNotFoundException

    為了回溯相容性,Android 會擲回 ClassNotFoundException 而不是 NoClassDefFoundError FindClass。此行為與 Java 反射 API 一致 Class.forName(name)

常見問題:為什麼要取得 UnsatisfiedLinkError

在原生程式碼中,您經常會看到類似以下的失敗情形:

java.lang.UnsatisfiedLinkError: Library foo not found

在某些情況下,這表示系統找不到程式庫。在其他情況下,程式庫雖然存在,但無法由 dlopen(3) 開啟,您可以在例外狀況的詳細訊息中找到失敗的詳細資料。

以下是可能導致「找不到程式庫」例外狀況的常見原因:

  • 程式庫不存在或應用程式無法存取。請使用 adb shell ls -l <path> 檢查程式庫是否存在及權限。
  • 程式庫並非使用 NDK 建構。這可能會導致依附元件依賴裝置上不存在的函式或程式庫。

其他 UnsatisfiedLinkError 失敗類別如下所示:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

您可以在 Logcat 中看到:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

這代表執行階段嘗試尋找相符的方法, 無法順利完成常見原因如下:

  • 無法載入程式庫。請檢查 logcat 輸出內容,查看有關程式庫載入的訊息。
  • 由於名稱或簽章不符,因此找不到方法。這通常是因為:
    • 對於延遲方法查詢,無法使用 extern "C" 和適當的瀏覽權限 (JNIEXPORT) 宣告 C++ 函式。請注意,在 Ice Cream Sandwich 之前,JNIEXPORT 巨集不正確,因此將新版 GCC 與舊版 jni.h 搭配使用將無法運作。您可以使用「arm-eabi-nm」 即可查看程式庫中的符號;尋找 雜質 (如 _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass 而非 Java_Foo_myfunc),或者如果符號類型是 小寫的「t」而不是大寫的「T」 請調整宣告內容
    • 對於明確註冊,輸入方法簽名時會發生輕微錯誤。請確認您傳遞至註冊呼叫的內容與記錄檔中的簽名相符。請記得「B」是 byte 和「Z」為 boolean。 簽名中的類別名稱元件開頭為「L」,結尾為「;」, 請使用「/」分隔套件/類別名稱,然後使用「$」分隔 內部類別名稱 (例如 Ljava/util/Map$Entry;)。

使用 javah 自動產生 JNI 標頭可能有所幫助 避免一些問題

常見問題:為什麼FindClass找不到我的課程?

(大多數關於尋找方法失敗的方法,大多同樣適用)。 GetMethodIDGetStaticMethodID,或已填寫多個欄位 GetFieldIDGetStaticFieldID)。

請確認類別名稱字串的格式正確無誤。JNI 類別 名稱以套件名稱開頭,並以正斜線分隔 例如 java/lang/String。如果您查詢的是陣列類別 請先使用適當的方括號數量 也必須以「L」納入類別和「;」,所以這是 String 應為 [Ljava/lang/String;。 如果您要尋找內部課程,請使用「$」而非「.」。一般來說 在 .class 檔案中使用 javap 有助於找出 類別的內部名稱。

如果您啟用程式碼縮減功能,請務必設定要保留的程式碼。設定中 適當的保留規則非常重要,因為程式碼縮減器可能會移除類別、方法 或是僅從 JNI 中使用的欄位

如果類別名稱正確,您可能是執行到類別載入器 問題。FindClass 想在與您的程式碼相關聯的類別載入器中,開始進行類別搜尋。檢查呼叫堆疊 看起來會像這樣:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

最上層的方法是 Foo.myfuncFindClass 會找出與 Foo 類別相關聯的 ClassLoader 物件,並使用該物件。

這通常可以達到您想要的效果。如果您自行建立執行緒 (例如呼叫 pthread_create,然後透過 AttachCurrentThread 附加),可能會發生問題。現在應用程式中沒有任何堆疊框架。如果您從此執行緒呼叫 FindClass, JavaVM 會以「system」而不是關聯類別載入器的 ,因此嘗試尋找應用程式特定類別時會失敗。

以下提供幾種因應方法:

  • 對你的 FindClass 執行一次查詢, JNI_OnLoad 的資料,以及快取類別參照,以供稍後使用 相關單位會如何運用資料,並讓他們覺得自己 獲得充分告知,且能夠針對該使用方式表示同意於執行期間發出的任何 FindClass 呼叫 JNI_OnLoad 會使用與 呼叫 System.loadLibrary 的函式 (這是 的特殊規則)。 如果應用程式的程式碼正在載入程式庫,請FindClass 就會使用正確的類別載入器
  • 將類別例項傳遞至需要的函式 方法是宣告原生方法,以接收類別引數 然後傳入 Foo.class
  • ClassLoader 物件的參照儲存在某處 並直接撥打 loadClass 電話。這需要 不容易。

常見問題:如何透過原生程式碼分享原始資料?

您可能會遇到需要存取大量資料的情況 由代管和原生程式碼產生原始資料的緩衝區。常見範例 包括操控點陣圖或聲音樣本有兩種基本做法。

您可以將資料儲存在 byte[] 中。這樣一來,您就能從受管理的程式碼快速存取。不過,在原生端,您不一定能夠在不複製資料的情況下存取資料。於 部分實作項目,GetByteArrayElementsGetPrimitiveArrayCritical 會傳回實際的指標 代管堆積中的原始資料,但在其他情況下會分配緩衝區 並複製資料

另一個做法是將資料儲存在直接位元組緩衝區中。這些物件可以使用 java.nio.ByteBuffer.allocateDirect 或 JNI NewDirectByteBuffer 函式建立。與一般 或位元組緩衝區,系統就不會在代管堆積上分配儲存空間 一律直接從原生程式碼存取 (取得位址 透過 GetDirectBufferAddress)。根據 導入位元組緩衝區存取權,從代管程式碼存取資料 執行速度可能非常慢

您可以根據以下兩個因素來選擇要使用哪一種:

  1. 大多數資料存取作業是否會從以 Java 或 C/C++ 編寫的程式碼執行?
  2. 如果資料最終會傳遞至系統 API, 電子郵件大概會是什麼樣子?(舉例來說,如果資料最終傳遞至 會接收位元組 [] ByteBuffer 不一定相同)。

如果沒有明顯的勝出組合,請使用直接位元組緩衝區。我們已直接在 JNI 中加入對這些功能的支援,日後版本的效能應會有所提升。