JNI 提示

JNI 是 Java Native Interface。用於定義 Android 從哪些位元碼編譯 用於與原生程式碼互動的代管程式碼 (以 Java 或 Kotlin 程式設計語言編寫) (以 C/C++ 撰寫)。JNI 支援從動態共用載入的程式碼,不受供應商限制 不僅耗時又費時,

注意:因為 Android 會將 Kotlin 編譯成支援 ART 的位元碼, 與 Java 程式設計語言類似,您可以使用本頁上的指南, 從 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。這些編號通常 內部執行階段資料結構的指標。查詢可能需要多個字串 但在您獲得實際呼叫以取得欄位或叫用方法後 執行速度非常快

如果效能很重要,建議您一次查看值並快取結果 。因為每個程序對一個 JavaVM 具有限制 以靜態的本機結構儲存這項資料

在卸載類別之前,類別參照、欄位 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 是不透明的 而不是物件參照,且不應將這些物件傳遞至 NewGlobalRef。原始資料 GetStringUTFChars 等函式傳回的指標 和 GetByteArrayElements 也是物件(可能會通過這些測試) ,且有效期限到相符的 Release 呼叫為止)。

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

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

UTF-8 和 UTF-16 字串

Java 程式設計語言使用 UTF-16。為了方便起見,JNI 提供可以 已修改 UTF-8。 經過修改的編碼處理為 C 程式碼非常有用,因為編碼會將 \u0000 編碼為 0xc0 0x80 而非 0x00。 值得一提的是,您可以放心使用 C 樣式的零結尾字串 適用於標準 libc 字串函式。但缺點是無法傳遞 轉換為 JNI 的任何 UTF-8 資料,且預期可正常運作。

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

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

傳遞至 NewStringUTF 的資料必須使用修改後的 UTF-8 格式。A 罩杯 常見錯誤是讀取檔案或網路串流中的字元資料 並交給「NewStringUTF」處理,不進行篩選 除非您知道資料是有效的 MUTF-8 (或 7 位元 ASCII,這是相容的子集) 您必須刪除無效字元,或將這些字元轉換為正確的修改 UTF-8 表單。 否則採用 UTF-16 轉換,可能會產生非預期的結果。 CheckJNI (模擬器預設啟用) 是掃描字串 如果 VM 收到無效的輸入,則會取消該 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 中宣告一樣。

為了盡可能提高介面效率,但不限制 Get<PrimitiveType>ArrayElements 的 VM 實作項目 呼叫系列可讓執行階段傳回實際元素的指標,或 分配一些記憶體並建立副本無論是哪一種方式 原始指標都會傳回 在對應的 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),然後傳回以下結果: 或是清除並處理例外狀況

在例外狀況: 尚待處理:

  • 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 指令 在目前的執行緒中設定例外狀況指標。回到代管模式時 ,系統就會處理並妥善處理例外狀況。

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

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

延伸檢查

JNI 幾乎不需要檢查錯誤。錯誤通常會導致當機。Android 也提供 CheckJNI 模式,其中 JavaVM 和 JNIEnv 函式資料表指標會切換為多個函式資料表,以便在呼叫標準實作項目之前執行一系列完整的檢查。

這些額外檢查包括:

  • 陣列:嘗試分配負大小的陣列。
  • 錯誤指標:將錯誤的 jarray/jclass/jobject/jstring 傳送至 JNI 呼叫,或將 NULL 指標傳送至具有非空值引數的 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) 初始化器。引數為「undecorated」圖書館名稱 因此,如要載入 libfubar.so,您需要傳入 "fubar"

如果只有一個包含原生方法的類別,呼叫 System.loadLibrary 設定為該類別的靜態初始化器。否則, 您想從 Application 發出呼叫,這樣您就會知道系統一律會載入程式庫 而且一律會提早載入

執行階段可以透過兩種方式尋找原生方法。您可以 向 RegisterNatives 註冊這些元件,或者您也可以讓執行階段動態查詢 dlsymRegisterNatives 的優勢在於 檢查符號是否存在,您也不必 但會匯出 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 並且通過 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 和 在 Android 8-10 中未實作 @CriticalNative 方法,以及 包含 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 輸出結果: 有關載入程式庫的訊息。
  • 由於名稱或簽章不符,因此找不到方法。這個 常見原因如下:
    • 無法宣告 C++ 函式 有extern "C"和合適的單位 瀏覽權限 (JNIEXPORT)。請注意,冰淇淋前 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 中,且在日後版本中的效能應該會有所提升。