NDK 系列(5):JNI 從入門到實踐肛循,爆肝萬字詳解!

請點贊關注团赁,你的支持對我意義重大 ?? ??

?? Hi,我是小彭谨履。本文已收錄到 GitHub · Android-NoteBook 中欢摄。這里有 Android 進階成長知識體系,有志同道合的朋友笋粟,帶你建立核心競爭力怀挠。

前言

  • 在 Android 生態(tài)中主要有 C/C++、Java害捕、Kotlin 三種語言 绿淋,它們的關系不是替換而是互補。其中尝盼,C/C++ 的語境是算法和高性能吞滞,Java 的語境是平臺無關和內(nèi)存管理,而 Kotlin 則融合了多種語言中的優(yōu)秀特性盾沫,帶來了一種更現(xiàn)代化的編程方式裁赠;
  • JNI 是實現(xiàn) Java 代碼與 C/C++ 代碼交互的特性, 思考一個問題 —— Java 虛擬機是如何實現(xiàn)兩種毫不相干的語言的交互的呢赴精? 今天佩捞,我們來全面總結 JNI 開發(fā)知識框架,為 NDK 開發(fā)打下基礎蕾哟。本文部分演示代碼可以從 DemoHall·HelloJni 下載查看一忱。

這篇文章是 NDK 系列文章第 5 篇,專欄文章列表:

一谭确、語言基礎:

  • 1意乓、NDK 學習路線:怎么學 & 我的經(jīng)驗
  • 2、C 語言基礎
  • 3拂檩、C ++ 語言基礎
  • 4蜓萄、C/C++ 編譯過程:從源碼到程序運行

二、NDK 開發(fā):

三郊艘、基礎理論

四、計算機基礎


JNI 學習路線圖:


1. 認識 JNI

1.1 為什么要使用 JNI?

JNI(Java Native Interface便监,Java 本地接口)是 Java 生態(tài)的特性扎谎,它擴展了 Java 虛擬機的能力,使得 Java 代碼可以與 C/C++ 代碼進行交互烧董。 通過 JNI 接口簿透,Java 代碼可以調(diào)用 C/C++ 代碼,C/C++ 代碼也可以調(diào)用 Java 代碼解藻。

這就引出第 1 個問題(為什么要這么做):Java 為什么要調(diào)用 C/C++ 代碼老充,而不是直接用 Java 開發(fā)需求呢?我認為主要有 4 個原因:

  • 原因 1 - Java 天然需要 JNI 技術: 雖然 Java 是平臺無關性語言螟左,但運行 Java 語言的虛擬機是運行在具體平臺上的啡浊,所以 Java 虛擬機是平臺相關的。因此胶背,對于調(diào)用平臺 API 的功能(例如打開文件功能巷嚣,在 Window 平臺是 openFile 函數(shù),而在 Linux 平臺是 open 函數(shù))時钳吟,雖然在 Java 語言層是平臺無關的廷粒,但背后只能通過 JNI 技術在 Native 層分別調(diào)用不同平臺 API。類似的,對于有操作硬件需求的程序坝茎,也只能通過 C/C++ 實現(xiàn)對硬件的操作涤姊,再通過 JNI 調(diào)用;
  • 原因 2 - Java 運行效率不及 C/C++: Java 代碼的運行效率相對于 C/C++ 要低一些嗤放,因此思喊,對于有密集計算(例如實時渲染、音視頻處理次酌、游戲引擎等)需求的程序恨课,會選擇用 C/C++ 實現(xiàn),再通過 JNI 調(diào)用岳服;
  • 原因 3 - Native 層代碼安全性更高: 反編譯 so 文件的難度比反編譯 Class 文件高剂公,一些跟密碼相關的功能會選擇用 C/C++ 實現(xiàn),再通過 JNI 調(diào)用吊宋;
  • 原因 4 - 復用現(xiàn)有代碼: 當 C/C++ 存在程序需要的功能時纲辽,則可以直接復用。

還有第 2 個問題(為什么可以這么做):為什么兩種獨立的語言可以實現(xiàn)交互呢贫母?因為 Java 虛擬機本身就是 C/C++ 實現(xiàn)的文兑,無論是 Java 代碼還是 C/C++ 代碼盒刚,最終都是由這個虛擬機支撐腺劣,共同使用一個進程空間。JNI 要做的只是在兩種語言之間做橋接因块。

1.2 JNI 開發(fā)的基本流程

一個標準的 JNI 開發(fā)流程主要包含以下步驟:

  • 1橘原、創(chuàng)建 HelloWorld.java,并聲明 native 方法 sayHi()涡上;
  • 2趾断、使用 javac 命令編譯源文件,生成 HelloWorld.class 字節(jié)碼文件吩愧;
  • 3芋酌、使用 javah 命令導出 HelloWorld.h 頭文件(頭文件中包含了本地方法的函數(shù)原型);
  • 4雁佳、在源文件 HelloWorld.cpp 中實現(xiàn)函數(shù)原型脐帝;
  • 5、編譯本地代碼糖权,生成 Hello-World.so 動態(tài)原生庫文件堵腹;
  • 6、在 Java 代碼中調(diào)用 System.loadLibrary(...) 加載 so 文件星澳;
  • 7疚顷、使用 Java 命令運行 HelloWorld 程序。

該流程用示意圖表示如下:

1.3 JNI 的性能誤區(qū)

JNI 本身本身并不能解決性能問題,錯誤地使用 JNI 反而可能引入新的性能問題腿堤,這些問題都是要注意的:

  • 問題 1 - 跨越 JNI 邊界的調(diào)用: 從 Java 調(diào)用 Native 或從 Native 調(diào)用 Java 的成本很高阀坏,使用 JNI 時要限制跨越 JNI 邊界的調(diào)用次數(shù);
  • 問題 2 - 引用類型數(shù)據(jù)的回收: 由于引用類型數(shù)據(jù)(例如字符串释液、數(shù)組)傳遞到 JNI 層的只是一個指針全释,為避免該對象被垃圾回收虛擬機會固定住(pin)對象误债,在 JNI 方法返回前會阻止其垃圾回收浸船。因此,要盡量縮短 JNI 調(diào)用的執(zhí)行時間寝蹈,它能夠縮短對象被固定的時間(關于引用類型數(shù)據(jù)的處理李命,在下文會說到)。

1.4 注冊 JNI 函數(shù)的方式

Java 的 native 方法和 JNI 函數(shù)是一一對應的映射關系箫老,建立這種映射關系的注冊方式有 2 種:

  • 方式 1 - 靜態(tài)注冊: 基于命名約定建立映射關系封字;
  • 方式 2 - 動態(tài)注冊: 通過 JNINativeMethod 結構體建立映射關系。

關于注冊 JNI 函數(shù)的更多原理分析耍鬓,見 注冊 JNI 函數(shù)阔籽。

1.5 加載 so 庫的時機

so 庫需要在運行時調(diào)用 System.loadLibrary(…) 加載,一般有 2 種調(diào)用時機:

  • 1牲蜀、在類靜態(tài)初始化中: 如果只在一個類或者很少類中使用到該 so 庫笆制,則最常見的方式是在類的靜態(tài)初始化塊中調(diào)用;
  • 2涣达、在 Application 初始化時調(diào)用: 如果有很多類需要使用到該 so 庫在辆,則可以考慮在 Application 初始化等場景中提前加載。

關于加載 so 庫的更多原理分析度苔,見 so 文件加載過程分析匆篓。


2. JNI 模板代碼

本節(jié)我們通過一個簡單的 HelloWorld 程序來幫助你熟悉 JNI 的模板代碼。

JNI Demo

JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi (JNIEnv *, jobject);

2.1 JNI 函數(shù)名

為什么 JNI 函數(shù)名要采用 Java_com_xurui_HelloWorld_sayHi 的命名方式呢寇窑?—— 這是 JNI 函數(shù)靜態(tài)注冊約定的函數(shù)命名規(guī)則鸦概。Java 的 native 方法和 JNI 函數(shù)是一一對應的映射關系,而建立這種映射關系的注冊方式有 2 種:靜態(tài)注冊 + 動態(tài)注冊甩骏。

其中窗市,靜態(tài)注冊是基于命名約定建立的映射關系,一個 Java 的 native 方法對應的 JNI 函數(shù)會采用約定的函數(shù)名横漏,即 Java_[類的全限定名 (帶下劃線)]_[方法名] 谨设。JNI 調(diào)用 sayHi() 方法時,就會從 JNI 函數(shù)庫中尋找函數(shù) Java_com_xurui_HelloWorld_sayHi()缎浇,更多內(nèi)容見 注冊 JNI 函數(shù)扎拣。

2.2 關鍵詞 JNIEXPORT

JNIEXPORT 是宏定義,表示一個函數(shù)需要暴露給共享庫外部使用時。JNIEXPORT 在 Window 和 Linux 上有不同的定義:

jni.h

// Windows 平臺 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)

// Linux 平臺:
#define JNIIMPORT
#define JNIEXPORT  __attribute__ ((visibility ("default")))

2.3 關鍵詞 JNICALL

JNICALL 是宏定義二蓝,表示一個函數(shù)是 JNI 函數(shù)誉券。JNICALL 在 Window 和 Linux 上有不同的定義:

jni.h

// Windows 平臺 :
#define JNICALL __stdcall // __stdcall 是一種函數(shù)調(diào)用參數(shù)的約定 ,表示函數(shù)的調(diào)用參數(shù)是從右往左。

// Linux 平臺:
#define JNICALL

2.4 參數(shù) jobject

jobject 類型是 JNI 層對于 Java 層應用類型對象的表示刊愚。每一個從 Java 調(diào)用的 native 方法踊跟,在 JNI 函數(shù)中都會傳遞一個當前對象的引用。區(qū)分 2 種情況:

  • 1鸥诽、靜態(tài) native 方法: 第二個參數(shù)為 jclass 類型商玫,指向 native 方法所在類的 Class 對象;
  • 2牡借、實例 native 方法: 第二個參數(shù)為 jobject 類型拳昌,指向調(diào)用 native 方法的對象。

2.5 JavaVM 和 JNIEnv 的作用

JavaVMJNIEnv 是定義在 jni.h 頭文件中最關鍵的兩個數(shù)據(jù)結構:

  • JavaVM: 代表 Java 虛擬機钠龙,每個 Java 進程有且僅有一個全局的 JavaVM 對象炬藤,JavaVM 可以跨線程共享;
  • JNIEnv: 代表 Java 運行環(huán)境碴里,每個 Java 線程都有各自獨立的 JNIEnv 對象沈矿,JNIEnv 不可以跨線程共享。

JavaVM 和 JNIEnv 的類型定義在 C 和 C++ 中略有不同咬腋,但本質(zhì)上是相同的羹膳,內(nèi)部由一系列指向虛擬機內(nèi)部的函數(shù)指針組成。 類似于 Java 中的 Interface 概念帝火,不同的虛擬機實現(xiàn)會從它們派生出不同的實現(xiàn)類溜徙,而向 JNI 層屏蔽了虛擬機內(nèi)部實現(xiàn)(例如在 Android ART 虛擬機中湃缎,它們的實現(xiàn)分別是 JavaVMExt 和 JNIEnvExt)犀填。

jni.h

struct _JNIEnv;
struct _JavaVM;

#if defined(__cplusplus)
// 如果定義了 __cplusplus 宏,則按照 C++ 編譯
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
// 按照 C 編譯
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
 * C++ 版本的 _JavaVM嗓违,內(nèi)部是對 JNIInvokeInterface* 的包裝
 */
struct _JavaVM {
    // 相當于 C 版本中的 JNIEnv
    const struct JNIInvokeInterface* functions;

    // 轉發(fā)給 functions 代理
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    ...
};

/*
 * C++ 版本的 JNIEnv九巡,內(nèi)部是對 JNINativeInterface* 的包裝
 */
struct _JNIEnv {
    // 相當于 C 版本的 JavaVM
    const struct JNINativeInterface* functions;

    // 轉發(fā)給 functions 代理
    jint GetVersion()
    { return functions->GetVersion(this); }
    ...
};

可以看到,不管是在 C 語言中還是在 C++ 中蹂季,JNINativeInterface*JNINativeInterface* 這兩個結構體指針才是 JavaVM 和 JNIEnv 的實體冕广。不過 C++ 中加了一層包裝,在語法上更簡潔偿洁,例如:

示例程序

// 在 C 語言中撒汉,要使用 (*env)->
// 注意看這一句:typedef const struct JNINativeInterface* JNIEnv;
(*env)->FindClass(env, "java/lang/String");

// 在 C++ 中,要使用 env->
// 注意看這一句:jclass FindClass(const char* name)
//{ return functions->FindClass(this, name); }
env->FindClass("java/lang/String");

后文提到的大量 JNI 函數(shù)涕滋,其實都是定義在 JNINativeInterface 和 JNINativeInterface 內(nèi)部的函數(shù)指針睬辐。

jni.h

/*
 * JavaVM
 */
struct JNIInvokeInterface {
    // 一系列函數(shù)指針
    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

/*
 * JNIEnv
 */
struct JNINativeInterface {
    // 一系列函數(shù)指針
    jint        (*GetVersion)(JNIEnv *);
    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);
    ...
};

3. 數(shù)據(jù)類型轉換

這一節(jié)我們來討論 Java 層與 Native 層之間的數(shù)據(jù)類型轉換。

3.1 Java 類型映射(重點理解)

JNI 對于 Java 的基礎數(shù)據(jù)類型(int 等)和引用數(shù)據(jù)類型(Object、Class溯饵、數(shù)組等)的處理方式不同侵俗。這個原理非常重要,理解這個原理才能理解后面所有 JNI 函數(shù)的設計思路:

  • 基礎數(shù)據(jù)類型: 會直接轉換為 C/C++ 的基礎數(shù)據(jù)類型丰刊,例如 int 類型映射為 jint 類型隘谣。由于 jint 是 C/C++ 類型,所以可以直接當作普通 C/C++ 變量使用啄巧,而不需要依賴 JNIEnv 環(huán)境對象寻歧;
  • 引用數(shù)據(jù)類型: 對象只會轉換為一個 C/C++ 指針,例如 Object 類型映射為 jobject 類型秩仆。由于指針指向 Java 虛擬機內(nèi)部的數(shù)據(jù)結構熄求,所以不可能直接在 C/C++ 代碼中操作對象,而是需要依賴 JNIEnv 環(huán)境對象逗概。另外弟晚,為了避免對象在使用時突然被回收,在本地方法返回前逾苫,虛擬機會固定(pin)對象卿城,阻止其 GC。

另外需要特別注意一點铅搓,基礎數(shù)據(jù)類型在映射時是直接映射瑟押,而不會發(fā)生數(shù)據(jù)格式轉換。例如星掰,Java char 類型在映射為 jchar 后舊是保持 Java 層的樣子多望,數(shù)據(jù)長度依舊是 2 個字節(jié),而字符編碼依舊是 UNT-16 編碼氢烘。

具體映射關系都定義在 jni.h 頭文件中怀偷,文件摘要如下:

jni.h

typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */ /* 注意:jchar 是 2 個字節(jié) */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */
typedef jint     jsize;

#ifdef __cplusplus
// 內(nèi)部的數(shù)據(jù)結構由虛擬機實現(xiàn),只能從虛擬機源碼看
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
...
// 說明我們接觸到到 jobject播玖、jclass 其實是一個指針
typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
...
#else /* not __cplusplus */
...
#endif /* not __cplusplus */

我將所有 Java 類型與 JNI 類型的映射關系總結為下表:

Java 類型 JNI 類型 描述 長度(字節(jié))
boolean jboolean unsigned char 1
byte jbyte signed char 1
char jchar unsigned short 2
short jshort signed short 2
int jint椎工、jsize signed int 4
long jlong signed long 8
float jfloat signed float 4
double jdouble signed double 8
Class jclass Class 類對象 1
String jstrting 字符串對象 /
Object jobject 對象 /
Throwable jthrowable 異常對象 /
boolean[] jbooleanArray 布爾數(shù)組 /
byte[] jbyteArray byte 數(shù)組 /
char[] jcharArray char 數(shù)組 /
short[] jshortArray short 數(shù)組 /
int[] jinitArray int 數(shù)組 /
long[] jlongArray long 數(shù)組 /
float[] jfloatArray float 數(shù)組 /
double[] jdoubleArray double 數(shù)組 /

3.2 字符串類型操作

上面提到 Java 對象會映射為一個 jobject 指針,那么 Java 中的 java.lang.String 字符串類型也會映射為一個 jobject 指針蜀踏∥桑可能是因為字符串的使用頻率實在是太高了,所以 JNI 規(guī)范還專門定義了一個 jobject 的派生類 jstring 來表示 Java String 類型果覆,這個相對特殊颅痊。

jni.h

// 內(nèi)部的數(shù)據(jù)結構還是看不到,由虛擬機實現(xiàn)
class _jstring : public _jobject {};
typedef _jstring*       jstring;

struct JNINativeInterface {
    // String 轉換為 UTF-8 字符串
    const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
    // 釋放 GetStringUTFChars 生成的 UTF-8 字符串
    void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
    // 構造新的 String 字符串
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    // 獲取 String 字符串的長度
    jsize       (*GetStringUTFLength)(JNIEnv*, jstring);
    // 將 String 復制到預分配的 char* 數(shù)組中
    void        (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);
};

由于 Java 與 C/C++ 默認使用不同的字符編碼局待,因此在操作字符數(shù)據(jù)時斑响,需要特別注意在 UTF-16 和 UTF-8 兩種編碼之間轉換吗讶。關于字符編碼,我們在 Unicode 和 UTF-8是什么關系恋捆? 這篇文章里討論過照皆,這里就簡單回顧一下:

  • Unicode: 統(tǒng)一化字符編碼標準,為全世界所有字符定義統(tǒng)一的碼點沸停,例如 U+0011膜毁;
  • UTF-8: Unicode 標準的實現(xiàn)編碼之一,使用 1~4 字節(jié)的變長編碼愤钾。UTF-8 編碼中的一字節(jié)編碼與 ASCII 編碼兼容瘟滨。
  • UTF-16: Unicode 標準的實現(xiàn)編碼之一,使用 2 / 4 字節(jié)的變長編碼能颁。UTF-16 是 Java String 使用的字符編碼杂瘸;
  • UTF-32: Unicode 標準的實現(xiàn)編碼之一,使用 4 字節(jié)定長編碼伙菊。

以下為 2 種較為常見的轉換場景:

  • 1败玉、Java String 對象轉換為 C/C++ 字符串: 調(diào)用 GetStringUTFChars 函數(shù)將一個 jstring 指針轉換為一個 UTF-8 的 C/C++ 字符串,并在不再使用時調(diào)用 ReleaseStringChars 函數(shù)釋放內(nèi)存镜硕;
  • 2运翼、構造 Java String 對象: 調(diào)用 NewStringUTF 函數(shù)構造一個新的 Java String 字符串對象。

我們直接看一段示例程序:

示例程序

// 示例 1:將 Java String 轉換為 C/C++ 字符串
jstring jStr = ...; // Java 層傳遞過來的 String
const char *str = env->GetStringUTFChars(jStr, JNI_FALSE);
if(!str) {
    // OutOfMemoryError
    return;
}
// 釋放 GetStringUTFChars 生成的 UTF-8 字符串
env->ReleaseStringUTFChars(jStr, str);

// 示例 2:構造 Java String 對象(將 C/C++ 字符串轉換為 Java String)
jstring newStr = env->NewStringUTF("在 Native 層構造 Java String");
if (newStr) {
    // 通過 JNIEnv 方法將 jstring 調(diào)用 Java 方法(jstring 本身就是 Java String 的映射兴枯,可以直接傳遞到 Java 層)
    ...
}

此處對 GetStringUTFChars 函數(shù)的第 3 個參數(shù) isCopy 做解釋:它是一個布爾值參數(shù)血淌,將決定使用拷貝模式還是復用模式:

  • 1、JNI_TRUE: 使用拷貝模式财剖,JVM 將拷貝一份原始數(shù)據(jù)來生成 UTF-8 字符串悠夯;
  • 2、JNI_FALSE: 使用復用模式躺坟,JVM 將復用同一份原始數(shù)據(jù)來生成 UTF-8 字符串沦补。復用模式絕不能修改字符串內(nèi)容,否則 JVM 中的原始字符串也會被修改瞳氓,打破 String 不可變性策彤。

另外還有一個基于范圍的轉換函數(shù):GetStringUTFRegion:預分配一塊字符數(shù)組緩沖區(qū)栓袖,然后將 String 數(shù)據(jù)復制到這塊緩沖區(qū)中匣摘。由于這個函數(shù)本身不會做任何內(nèi)存分配,所以不需要調(diào)用對應的釋放資源函數(shù)裹刮,也不會拋出 OutOfMemoryError音榜。另外,GetStringUTFRegion 這個函數(shù)會做越界檢查并拋出 StringIndexOutOfBoundsException 異常捧弃。

示例程序

jstring jStr = ...; // Java 層傳遞過來的 String
char outbuf[128];
int len = env->GetStringLength(jStr);
env->GetStringUTFRegion(jStr, 0, len, outbuf);

3.3 數(shù)組類型操作

與 jstring 的處理方式類似赠叼,JNI 規(guī)范將 Java 數(shù)組定義為 jobject 的派生類 jarray

  • 基礎類型數(shù)組:定義為 jbooleanArray 擦囊、jintArray 等;
  • 引用類型數(shù)組:定義為 jobjectArray 嘴办。

下面區(qū)分基礎類型數(shù)組和引用類型數(shù)組兩種情況:

操作基礎類型數(shù)組(以 jintArray 為例):

  • 1瞬场、Java 基本類型數(shù)組轉換為 C/C++ 數(shù)組: 調(diào)用 GetIntArrayElements 函數(shù)將一個 jintArray 指針轉換為 C/C++ int 數(shù)組;
  • 2涧郊、修改 Java 基本類型數(shù)組: 調(diào)用 ReleaseIntArrayElements 函數(shù)并使用模式 0贯被;
  • 3、構造 Java 基本類型數(shù)組: 調(diào)用 NewIntArray 函數(shù)構造 Java int 數(shù)組妆艘。

我們直接看一段示例程序:

示例程序

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_xurui_hellojni_HelloWorld_generateIntArray(JNIEnv *env, jobject thiz, jint size) {
    // 新建 Java int[]
    jintArray jarr = env->NewIntArray(size);
    // 轉換為 C/C ++ int[]
    int *carr = env->GetIntArrayElements(jarr, JNI_FALSE);
    // 賦值
    for (int i = 0; i < size; i++) {
        carr[i] = i;
    }
    // 釋放資源并回寫
    env->ReleaseIntArrayElements(jarr, carr, 0);
    // 返回數(shù)組
    return jarr;
}

此處重點對 ReleaseIntArrayElements 函數(shù)的第 3 個參數(shù) mode 做解釋:它是一個模式參數(shù):

參數(shù) mode 描述
0 將 C/C++ 數(shù)組的數(shù)據(jù)回寫到 Java 數(shù)組彤灶,并釋放 C/C++ 數(shù)組
JNI_COMMIT 將 C/C++ 數(shù)組的數(shù)據(jù)回寫到 Java 數(shù)組,并不釋放 C/C++ 數(shù)組
JNI_ABORT 不回寫數(shù)據(jù)批旺,但釋放 C/C++ 數(shù)組

另外 JNI 還提供了基于范圍函數(shù):GetIntArrayRegionSetIntArrayRegion幌陕,使用方法和注意事項和 GetStringUTFRegion 也是類似的,也是基于一塊預分配的數(shù)組緩沖區(qū)汽煮。

操作引用類型數(shù)組(jobjectArray):

  • 1搏熄、將 Java 引用類型數(shù)組轉換為 C/C++ 數(shù)組: 不支持!與基本類型數(shù)組不同暇赤,引用類型數(shù)組的元素 jobject 是一個指針搬卒,不存在轉換為 C/C++ 數(shù)組的概念;
  • 2翎卓、修改 Java 引用類型數(shù)組: 調(diào)用 SetObjectArrayElement 函數(shù)修改指定下標元素契邀;
  • 3、構造 Java 引用類型數(shù)組: 先調(diào)用 FindClass 函數(shù)獲取 Class 對象失暴,再調(diào)用 NewObjectArray 函數(shù)構造對象數(shù)組坯门。

我們直接看一段示例程序:

示例程序

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_xurui_hellojni_HelloWorld_generateStringArray(JNIEnv *env, jobject thiz, jint size) {
    // 獲取 String Class
    jclass jStringClazz = env->FindClass("java/lang/String");
    // 初始值(可為空)
    jstring initialStr = env->NewStringUTF("初始值");
    // 創(chuàng)建 Java String[]
    jobjectArray jarr = env->NewObjectArray(size, jStringClazz, initialStr);
    // 賦值
    for (int i = 0; i < size; i++) {
        char str[5];
        sprintf(str, "%d", i);
        jstring jStr = env->NewStringUTF(str);
        env->SetObjectArrayElement(jarr, i, jStr);
    }
    // 返回數(shù)組
    return jarr;
}

4. JNI 訪問 Java 字段與方法

這一節(jié)我們來討論如何從 Native 層訪問 Java 的字段與方法。在開始訪問前逗扒,JNI 首先要找到想訪問的字段和方法古戴,這就依靠字段描述符和方法描述符。

4.1 字段描述符與方法描述符

在 Java 源碼中定義的字段和方法矩肩,在編譯后都會按照既定的規(guī)則記錄在 Class 文件中的字段表和方法表結構中现恼。例如,一個 public String str; 字段會被拆分為字段訪問標記(public)黍檩、字段簡單名稱(str)和字段描述符(Ljava/lang/String)叉袍。 因此,從 JNI 訪問 Java 層的字段或方法時刽酱,首先就是要獲取在 Class 文件中記錄的簡單名稱和描述符喳逛。

Class 文件的一級結構:

字段表結構: 包含字段的訪問標記、簡單名稱棵里、字段描述符等信息润文。例如字段 String str 的簡單名稱為 str姐呐,字段描述符為 Ljava/lang/String;

方法表結構: 包含方法的訪問標記、簡單名稱典蝌、方法描述符等信息曙砂。例如方法 void fun(); 的簡單名稱為 fun,方法描述符為 ()V

4.2 描述符規(guī)則

  • 字段描述符: 字段描述符其實就是描述字段的類型骏掀,JVM 對每種基礎數(shù)據(jù)類型定義了固定的描述符麦轰,而引用類型則是以 L 開頭的形式:
Java 類型 描述符
boolean Z
byte B
char C
short S
int I
long J
floag F
double D
void V
引用類型 以 L 開頭 ; 結尾,中間是 / 分隔的包名和類名砖织。例如 String 的字段描述符為 Ljava/lang/String;
  • 方法描述符: 方法描述符其實就是描述方法的返回值類型和參數(shù)表類型款侵,參數(shù)類型用一對圓括號括起來,按照參數(shù)聲明順序列舉參數(shù)類型侧纯,返回值出現(xiàn)在括號后面新锈。例如方法 void fun(); 的簡單名稱為 fun,方法描述符為 ()V

4.3 JNI 訪問 Java 字段

本地代碼訪問 Java 字段的流程分為 2 步:

  • 1眶熬、通過 jclass 獲取字段 ID妹笆,例如:Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;");
  • 2、通過字段 ID 訪問字段娜氏,例如:Jstr = env->GetObjectField(thiz, Fid);

Java 字段分為靜態(tài)字段和實例字段拳缠,相關方法如下:

  • GetFieldId:獲取實例方法的字段 ID
  • GetStaticFieldId:獲取靜態(tài)方法的字段 ID
  • Get<Type>Field:獲取類型為 Type 的實例字段(例如 GetIntField)
  • Set<Type>Field:設置類型為 Type 的實例字段(例如 SetIntField)
  • GetStatic<Type>Field:獲取類型為 Type 的靜態(tài)字段(例如 GetStaticIntField)
  • SetStatic<Type>Field:設置類型為 Type 的靜態(tài)字段(例如 SetStaticIntField)

示例程序

extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessField(JNIEnv *env, jobject thiz) {
    // 獲取 jclass
    jclass clz = env->GetObjectClass(thiz);
    // 示例:修改 Java 靜態(tài)變量值
    // 靜態(tài)字段 ID
    jfieldID sFieldId = env->GetStaticFieldID(clz, "sName", "Ljava/lang/String;");
    // 訪問靜態(tài)字段
    if (sFieldId) {
        // Java 方法的返回值 String 映射為 jstring
        jstring jStr = static_cast<jstring>(env->GetStaticObjectField(clz, sFieldId));
        // 將 jstring 轉換為 C 風格字符串
        const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);
        // 釋放資源
        env->ReleaseStringUTFChars(jStr, sStr);
        // 構造 jstring
        jstring newStr = env->NewStringUTF("靜態(tài)字段 - Peng");
        if (newStr) {
            // jstring 本身就是 Java String 的映射,可以直接傳遞到 Java 層
            env->SetStaticObjectField(clz, sFieldId, newStr);
        }
    }
    // 示例:修改 Java 成員變量值
    // 實例字段 ID
    jfieldID mFieldId = env->GetFieldID(clz, "mName", "Ljava/lang/String;");
    // 訪問實例字段
    if (mFieldId) {
        jstring jStr = static_cast<jstring>(env->GetObjectField(thiz, mFieldId));
        // 轉換為 C 字符串
        const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);
        // 釋放資源
        env->ReleaseStringUTFChars(jStr, sStr);
        // 構造 jstring
        jstring newStr = env->NewStringUTF("實例字段 - Peng");
        if (newStr) {
            // jstring 本身就是 Java String 的映射贸弥,可以直接傳遞到 Java 層
            env->SetObjectField(thiz, mFieldId, newStr);
        }
    }
}

4.4 JNI 調(diào)用 Java 方法

本地代碼訪問 Java 方法與訪問 Java 字段類似窟坐,訪問流程分為 2 步:

  • 1、通過 jclass 獲取「方法 ID」绵疲,例如:Mid = env->GetMethodID(jclass, "helloJava", "()V");
  • 2哲鸳、通過方法 ID 調(diào)用方法,例如:env->CallVoidMethod(thiz, Mid);

Java 方法分為靜態(tài)方法和實例方法盔憨,相關方法如下:

  • GetMethodId:獲取實例方法 ID
  • GetStaticMethodId:獲取靜態(tài)方法 ID
  • Call<Type>Method:調(diào)用返回類型為 Type 的實例方法(例如 GetVoidMethod)
  • CallStatic<Type>Method:調(diào)用返回類型為 Type 的靜態(tài)方法(例如 CallStaticVoidMethod)
  • CallNonvirtual<Type>Method:調(diào)用返回類型為 Type 的父類方法(例如 CallNonvirtualVoidMethod)

示例程序

extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessMethod(JNIEnv *env, jobject thiz) {
    // 獲取 jclass
    jclass clz = env->GetObjectClass(thiz);
    // 示例:調(diào)用 Java 靜態(tài)方法
    // 靜態(tài)方法 ID
    jmethodID sMethodId = env->GetStaticMethodID(clz, "sHelloJava", "()V");
    if (sMethodId) {
        env->CallStaticVoidMethod(clz, sMethodId);
    }
    // 示例:調(diào)用 Java 實例方法
    // 實例方法 ID
    jmethodID mMethodId = env->GetMethodID(clz, "helloJava", "()V");
    if (mMethodId) {
        env->CallVoidMethod(thiz, mMethodId);
    }
}

4.5 緩存 ID

訪問 Java 層字段或方法時徙菠,需要先利用字段名 / 方法名和描述符進行檢索,獲得 jfieldID / jmethodID郁岩。這個檢索過程比較耗時婿奔,優(yōu)化方法是將字段 ID 和方法 ID 緩存起來,減少重復檢索问慎。

提示: 從不同線程中獲取同一個字段或方法 的 ID 是相同的萍摊,緩存 ID 不會有多線程問題。

緩存字段 ID 和 方法 ID 的方法主要有 2 種:

  • 1蝴乔、使用時緩存: 使用時緩存是指在首次訪問字段或方法時记餐,將字段 ID 或方法 ID 存儲在靜態(tài)變量中。這樣將來再次調(diào)用本地方法時薇正,就不需要重復檢索 ID 了片酝。例如:
  • 2、類初始化時緩存: 靜態(tài)初始化時緩存是指在 Java 類初始化的時候挖腰,提前緩存字段 ID 和方法 ID雕沿。可以選擇在 JNI_OnLoad 方法中緩存猴仑,也可以在加載 so 庫后調(diào)用一個 native 方法進行緩存审轮。

兩種緩存 ID 方式的主要區(qū)別在于緩存發(fā)生的時機和時效性:

  • 1、時機不同: 使用時緩存是延遲按需緩存辽俗,只有在首次訪問 Java 時才會獲取 ID 并緩存疾渣,而類初始化時緩存是提前緩存;
  • 2崖飘、時效性不同: 使用時緩存的 ID 在類卸載后失效榴捡,在類卸載后不能使用,而類加載時緩存在每次加載 so 動態(tài)庫時會重新更新緩存朱浴,因此緩存的 ID 是保持有效的吊圾。

5. JNI 中的對象引用管理

5.1 Java 和 C/C++ 中對象內(nèi)存回收區(qū)別(重點理解)

在討論 JNI 中的對象引用管理,我們先回顧一下 Java 和 C/C++ 在對象內(nèi)存回收上的區(qū)別:

  • Java: 對象在堆 / 方法區(qū)上分配翰蠢,由垃圾回收器掃描對象可達性進行回收项乒。如果使用局部變量指向?qū)ο螅诓辉偈褂脤ο髸r可以手動顯式置空梁沧,也可以等到方法返回時自動隱式置空檀何。如果使用全局變量(static)指向?qū)ο螅诓辉偈褂脤ο髸r必須手動顯式置空廷支。
  • C/C++: 棧上分配的對象會在方法返回時自動回收埃碱,而堆上分配的對象不會隨著方法返回而回收,也沒有垃圾回收器管理酥泞,因此必須手動回收(free/delete)砚殿。

而 JNI 層作為 Java 層和 C/C++ 層之間的橋接層,那么它就會兼具兩者的特點:對于

  • 局部 Java 對象引用: 在 JNI 層可以通過 NewObject 等函數(shù)創(chuàng)建 Java 對象,并且返回對象的引用,這個引用就是 Local 型的局部引用叮雳。對于局部引用从隆,可以通過 DeleteLocalRef 函數(shù)手動顯式釋放(這類似于在 Java 中顯式置空局部變量),也可以等到函數(shù)返回時自動釋放(這類似于在 Java 中方法返回時隱式置空局部變量)芙贫;
  • 全局 Java 對象引用: 由于局部引用在函數(shù)返回后一定會釋放,可以通過 NewGlobalRef 函數(shù)將局部引用升級為 Global 型全局變量,這樣就可以在方法使用對象(這類似于在 Java 中使用 static 變量指向?qū)ο螅┢袜隆T诓辉偈褂脤ο髸r必須調(diào)用 DeleteGlobalRef 函數(shù)釋放全局引用(這類似于在 Java 中顯式置空 static 變量)。

提示: 我們這里所說的 ”置空“ 只是將指向變量的值賦值為 null先壕,而不是回收對象瘩扼,Java 對象回收是交給垃圾回收器處理的谆甜。

5.2 JNI 中的三種引用

  • 1、局部引用: 大部分 JNI 函數(shù)會創(chuàng)建局部引用集绰,局部引用只有在創(chuàng)建引用的本地方法返回前有效规辱,也只在創(chuàng)建局部引用的線程中有效。在方法返回后栽燕,局部引用會自動釋放罕袋,也可以通過 DeleteLocalRef 函數(shù)手動釋放;
  • 2碍岔、全局引用: 局部引用要跨方法和跨線程必須升級為全局引用浴讯,全局引用通過 NewGlobalRef 函數(shù)創(chuàng)建,不再使用對象時必須通過 DeleteGlobalRef 函數(shù)釋放蔼啦。
  • 3榆纽、弱全局引用: 弱引用與全局引用類似,區(qū)別在于弱全局引用不會持有強引用询吴,因此不會阻止垃圾回收器回收引用指向的對象掠河。弱全局引用通過 NewGlobalWeakRef 函數(shù)創(chuàng)建,不再使用對象時必須通過 DeleteGlobalWeakRef 函數(shù)釋放猛计。

示例程序

// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
env->DeleteLocalRef(localRefClz);

// 全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
env->DeleteGlobalRef(globalRefClz);

// 弱全局引用
jclass weakRefClz = env->NewWeakGlobalRef(localRefClz);
env->DeleteGlobalWeakRef(weakRefClz);

5.3 JNI 引用的實現(xiàn)原理

在 JavaVM 和 JNIEnv 中唠摹,會分別建立多個表管理引用:

  • JavaVM 內(nèi)有 globals 和 weak_globals 兩個表管理全局引用和弱全局引用。由于 JavaVM 是進程共享的奉瘤,因此全局引用可以跨方法和跨線程共享勾拉;
  • JavaEnv 內(nèi)有 locals 表管理局部引用,由于 JavaEnv 是線程獨占的盗温,因此局部引用不能跨線程藕赞。另外虛擬機在進入和退出本地方法通過 Cookie 信息記錄哪些局部引用是在哪些本地方法中創(chuàng)建的,因此局部引用是不能跨方法的卖局。

5.4 比較引用是否指向相同對象

可以使用 JNI 函數(shù) IsSameObject 判斷兩個引用是否指向相同對象(適用于三種引用類型)斧蜕,返回值為 JNI_TRUE 時表示相同,返回值為 JNI_FALSE 表示不同砚偶。例如:

示例程序

jclass localRef = ...
jclass globalRef = ...
bool isSampe = env->IsSamObject(localRef, globalRef)

另外批销,當引用與 NULL 比較時含義略有不同:

  • 局部引用和全局引用與 NULL 比較: 用于判斷引用是否指向 NULL 對象;
  • 弱全局引用與 NULL 比較: 用于判斷引用指向的對象是否被回收染坯。

6. JNI 中的異常處理

6.1 JNI 的異常處理機制(重點理解)

JNI 中的異常機制與 Java 和 C/C++ 的處理機制都不同:

  • Java 和 C/C++: 程序使用關鍵字 throw 拋出異常均芽,虛擬機會中斷當前執(zhí)行流程,轉而去尋找匹配的 catch{} 塊单鹿,或者繼續(xù)向外層拋出尋找匹配 catch {} 塊掀宋。
  • JNI: 程序使用 JNI 函數(shù) ThrowNew 拋出異常,程序不會中斷當前執(zhí)行流程,而是返回 Java 層后劲妙,虛擬機才會拋出這個異常湃鹊。

因此,在 JNI 層出現(xiàn)異常時是趴,有 2 種處理選擇:

  • 方法 1: 直接 return 當前方法涛舍,讓 Java 層去處理這個異常(這類似于在 Java 中向方法外層拋出異常)澄惊;
  • 方法 2: 通過 JNI 函數(shù) ExceptionClear 清除這個異常唆途,再執(zhí)行異常處理程序(這類似于在 Java 中 try-catch 處理異常)。需要注意的是掸驱,當異常發(fā)生時肛搬,必須先處理-清除異常,再執(zhí)行其他 JNI 函數(shù)調(diào)用毕贼。 因為當運行環(huán)境存在未處理的異常時温赔,只能調(diào)用 2 種 JNI 函數(shù):異常護理函數(shù)和清理資源函數(shù)。

JNI 提供了以下與異常處理相關的 JNI 函數(shù):

  • ThrowNew: 向 Java 層拋出異常鬼癣;
  • ExceptionDescribe: 打印異常描述信息陶贼;
  • ExceptionOccurred: 檢查當前環(huán)境是否發(fā)生異常,如果存在異常則返回該異常對象待秃;
  • ExceptionCheck: 檢查當前環(huán)境是否發(fā)生異常拜秧,如果存在異常則返回 JNI_TRUE,否則返回 JNI_FALSE章郁;
  • ExceptionClear: 清除當前環(huán)境的異常枉氮。

jni.h

struct JNINativeInterface {
    // 拋出異常
    jint        (*ThrowNew)(JNIEnv *, jclass, const char *);
    // 檢查異常
    jthrowable  (*ExceptionOccurred)(JNIEnv*);
    // 檢查異常
    jboolean    (*ExceptionCheck)(JNIEnv*);
    // 清除異常
    void        (*ExceptionClear)(JNIEnv*);
};

示例程序

// 示例 1:向 Java 層拋出異常
jclass exceptionClz = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(exceptionClz, "來自 Native 的異常");
// 示例 2:檢查當前環(huán)境是否發(fā)生異常(類似于 Java try{})
jthrowable exc = env->ExceptionOccurred(env);
if(exc) {
    // 處理異常(類似于 Java 的 catch{})
}
// 示例 3:清除異常
env->ExceptionClear();

6.2 檢查是否發(fā)生異常的方式

異常處理的步驟我懂了,由于虛擬機在遇到 ThrowNew 時不會中斷當前執(zhí)行流程暖庄,那我怎么知道當前已經(jīng)發(fā)生異常呢聊替?有 2 種方法:

  • 方法 1: 通過函數(shù)返回值錯誤碼,大部分 JNI 函數(shù)和庫函數(shù)都會有特定的返回值來標示錯誤培廓,例如 -1惹悄、NULL 等。在程序流程中可以多檢查函數(shù)返回值來判斷異常肩钠。
  • 方法 2: 通過 JNI 函數(shù) ExceptionOccurredExceptionCheck 檢查當前是否有異常發(fā)生泣港。

7. JNI 與多線程

這一節(jié)我們來討論 JNI 層中的多線程操作。

7.1 不能跨線程的引用

在 JNI 中蔬将,有 2 類引用是無法跨線程調(diào)用的爷速,必須時刻謹記:

  • JNIEnv: JNIEnv 只在所在的線程有效,在不同線程中調(diào)用 JNI 函數(shù)時霞怀,必須使用該線程專門的 JNIEnv 指針惫东,不能跨線程傳遞和使用。通過 AttachCurrentThread 函數(shù)將當前線程依附到 JavaVM 上,獲得屬于當前線程的 JNIEnv 指針廉沮。如果當前線程已經(jīng)依附到 JavaVM颓遏,也可以直接使用 GetEnv 函數(shù)。

示例程序

JNIEnv * env_child;
vm->AttachCurrentThread(&env_child, nullptr);
// 使用 JNIEnv*
vm->DetachCurrentThread();
  • 局部引用: 局部引用只在創(chuàng)建的線程和方法中有效滞时,不能跨線程使用叁幢。可以將局部引用升級為全局引用后跨線程使用坪稽。

示例程序

// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
// 釋放全局引用(非必須)
env->DeleteLocalRef(localRefClz);
// 局部引用升級為全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
// 釋放全局引用(必須)
env->DeleteGlobalRef(globalRefClz);

7.2 監(jiān)視器同步

在 JNI 中也會存在多個線程同時訪問一個內(nèi)存資源的情況曼玩,此時需要保證并發(fā)安全。在 Java 中我們會通過 synchronized 關鍵字來實現(xiàn)互斥塊(背后是使用監(jiān)視器字節(jié)碼)窒百,在 JNI 層也提供了類似效果的 JNI 函數(shù):

  • MonitorEnter: 進入同步塊黍判,如果另一個線程已經(jīng)進入該 jobject 的監(jiān)視器,則當前線程會阻塞篙梢;
  • MonitorExit: 退出同步塊顷帖,如果當前線程未進入該 jobject 的監(jiān)視器,則會拋出 IllegalMonitorStateException 異常渤滞。

jni.h

struct JNINativeInterface {
    jint        (*MonitorEnter)(JNIEnv*, jobject);
    jint        (*MonitorExit)(JNIEnv*, jobject);
}

示例程序

// 進入監(jiān)視器
if (env->MonitorEnter(obj) != JNI_OK) {
    // 建立監(jiān)視器的資源分配不成功等
}

// 此處為同步塊
if (env->ExceptionOccurred()) {
    // 必須保證有對應的 MonitorExit贬墩,否則可能出現(xiàn)死鎖
    if (env->MonitorExit(obj) != JNI_OK) {
        ...
    };
    return;
}

// 退出監(jiān)視器
if (env->MonitorExit(obj) != JNI_OK) {
    ...
};

7.3 等待與喚醒

JNI 沒有提供 Object 的 wati/notify 相關功能的函數(shù),需要通過 JNI 調(diào)用 Java 方法的方式來實現(xiàn):

示例程序

static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;

void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout) {
    env->CallVoidMethod(object, MID_Object_wait, timeout);
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object) {
    env->CallVoidMethod(object, MID_Object_notify);
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object) {
    env->CallVoidMethod(object, MID_Object_notifyAll);
}

7.4 創(chuàng)建線程的方法

在 JNI 開發(fā)中妄呕,有兩種創(chuàng)建線程的方式:

  • 方法 1 - 通過 Java API 創(chuàng)建: 使用我們熟悉的 Thread#start() 可以創(chuàng)建線程陶舞,優(yōu)點是可以方便地設置線程名稱和調(diào)試;
  • 方法 2 - 通過 C/C++ API 創(chuàng)建: 使用 pthread_create()std::thread 也可以創(chuàng)建線程

示例程序

// 
void *thr_fn(void *arg) {
    printids("new thread: ");
    return NULL;
}

int main(void) {
    pthread_t ntid;
    // 第 4 個參數(shù)將傳遞到 thr_fn 的參數(shù) arg 中
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("can't create thread: %s\n", strerror(err));
    }
    return 0;
}

8. 通用 JNI 開發(fā)模板

光說不練假把式趴腋,以下給出一個簡單的 JNI 開發(fā)模板吊说,將包括上文提到的一些比較重要的知識點。程序邏輯很簡單:Java 層傳遞一個媒體文件路徑到 Native 層后优炬,由 Native 層播放媒體并回調(diào)到 Java 層颁井。為了程序簡化,所有真實的媒體播放代碼都移除了蠢护,只保留模板代碼雅宾。

  • Java 層:start() 方法開始,調(diào)用 startNative() 方法進入 Native 層葵硕;
  • Native 層: 創(chuàng)建 MediaPlayer 對象眉抬,其中在子線程播放媒體文件,并通過預先持有的 JavaVM 指針獲取子線程的 JNIEnv 對象回調(diào)到 Java 層 onStarted() 方法懈凹。

MediaPlayer.kt

// Java 層模板
class MediaPlayer {
    companion object {
        init {
            // 注意點:加載 so 庫
            System.loadLibrary("hellondk")
        }
    }

    // Native 層指針
    private var nativeObj: Long? = null

    fun start(path : String) {
        // 注意點:記錄 Native 層指針蜀变,后續(xù)操作才能拿到 Native 的對象
        nativeObj = startNative(path)
    }

    fun release() {
        // 注意點:使用 start() 中記錄的指針調(diào)用 native 方法
        nativeObj?.let {
            releaseNative(it)
        }
        nativeObj = null
    }

    private external fun startNative(path : String): Long
    private external fun releaseNative(nativeObj: Long)

    fun onStarted() {
        // Native 層回調(diào)(來自 JNICallbackHelper#onStarted)
        ...
    }
}

native-lib.cpp

// 注意點:記錄 JavaVM 指針,用于在子線程獲得 JNIEnv
JavaVM *vm = nullptr;

jint JNI_OnLoad(JavaVM *vm, void *args) {
    ::vm = vm;
    return JNI_VERSION_1_6;
}

extern "C"
JNIEXPORT jlong JNICALL
Java_com_pengxr_hellondk_MediaPlayer_startNative(JNIEnv *env, jobject thiz, jstring path) {
    // 注意點:String 轉 C 風格字符串
    const char *path_ = env->GetStringUTFChars(path, nullptr);
    // 構造一個 Native 對象
    auto *helper = new JNICallbackHelper(vm, env, thiz);
    auto *player = new MediaPlayer(path_, helper);
    player->start();
    // 返回 Native 對象的指針
    return reinterpret_cast<jlong>(player);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_pengxr_hellondk_MediaPlayer_releaseNative(JNIEnv *env, jobject thiz, jlong native_obj) {
    auto * player = reinterpret_cast<MediaPlayer *>(native_obj);
    player->release();
}

JNICallbackHelper.h

#ifndef HELLONDK_JNICALLBACKHELPER_H
#define HELLONDK_JNICALLBACKHELPER_H

#include <jni.h>
#include "util.h"

class JNICallbackHelper {

private:
    // 全局共享的 JavaVM*
    // 注意點:指針要初始化 0 值
    JavaVM *vm = 0;
    // 主線程的 JNIEnv*
    JNIEnv *env = 0;
    // Java 層的對象 MediaPlayer.kt
    jobject job;
    // Java 層的方法 MediaPlayer#onStarted()
    jmethodID jmd_prepared;

public:
    JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job);

    ~JNICallbackHelper();

    void onStarted();
};

#endif //HELLONDK_JNICALLBACKHELPER_H

JNICallbackHelper.cpp

#include "JNICallbackHelper.h"

JNICallbackHelper::JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job) {
    // 全局共享的 JavaVM*
    this->vm = vm;
    // 主線程的 JNIEnv*
    this->env = env;

    // C 回調(diào) Java
    jclass mediaPlayerKTClass = env->GetObjectClass(job);
    jmd_prepared = env->GetMethodID(mediaPlayerKTClass, "onPrepared", "()V");

    // 注意點:jobject 無法跨越線程介评,需要轉換為全局引用
    // Error:this->job = job;
    this->job = env->NewGlobalRef(job);
}

JNICallbackHelper::~JNICallbackHelper() {
    vm = nullptr;
    // 注意點:釋放全局引用
    env->DeleteGlobalRef(job);
    job = nullptr;
    env = nullptr;
}

void JNICallbackHelper::onStarted() {
    // 注意點:子線程不能直接使用持有的主線程 env库北,需要通過 AttachCurrentThread 獲取子線程的 env
    JNIEnv * env_child;
    vm->AttachCurrentThread(&env_child, nullptr);
    // 回調(diào) Java 方法
    env_child->CallVoidMethod(job, jmd_prepared);
    vm->DetachCurrentThread();
}

MediaPlayer.h

#ifndef HELLONDK_MEDIAPLAYER_H
#define HELLONDK_MEDIAPLAYER_H

#include <cstring>
#include <pthread.h>
#include "JNICallbackHelper.h"

class MediaPlayer {
private:
    char *path = 0;
    JNICallbackHelper *helper = 0;
    pthread_t pid_start;
public:
    MediaPlayer(const char *path, JNICallbackHelper *helper);

    ~MediaPlayer();

    void doOpenFile();

    void start();

    void release();
};

#endif //HELLONDK_MEDIAPLAYER_H

MediaPlayer.cpp

#include "MediaPlayer.h"

MediaPlayer::MediaPlayer(const char *path, JNICallbackHelper *helper) {
    // 注意點:參數(shù) path 指向的空間被回收會造成懸空指針爬舰,應復制一份
    // this->path = path;
    this->path = new char[strlen(path) + 1];
    strcpy(this->path, path);

    this->helper = helper;
}

MediaPlayer::~MediaPlayer() {
    if (path) {
        delete path;
    }
    if (helper) {
        delete helper;
    }
}

// 在子線程執(zhí)行
void MediaPlayer::doOpenFile() {
    // 省略真實播放邏輯...
    // 媒體文件打開成功
    helper->onStarted();
}

// 在子線程執(zhí)行
void *task_open(void *args) {
    // args 是 主線程 MediaPlayer 的實例的 this變量
    auto *player = static_cast<MediaPlayer *>(args);
    player->doOpenFile();

    return nullptr;
}

void MediaPlayer::start() {
    // 切換到子線程執(zhí)行
    pthread_create(&pid_start, 0, task_open, this);
}

void MediaPlayer::release() {
    ...
}

9. 總結

到這里,JNI 的知識就講完了寒瓦,你可以按照學習路線圖來看情屹。下一篇,我們開始講 Android NDK 開發(fā)杂腰。關注我垃你,帶你建立核心競爭力,我們下次見喂很。


參考資料

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惜颇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恤筛,更是在濱河造成了極大的恐慌官还,老刑警劉巖芹橡,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毒坛,死亡現(xiàn)場離奇詭異,居然都是意外死亡林说,警方通過查閱死者的電腦和手機煎殷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腿箩,“玉大人豪直,你說我怎么就攤上這事≈橐疲” “怎么了弓乙?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長钧惧。 經(jīng)常有香客問我暇韧,道長,這世上最難降的妖魔是什么浓瞪? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任懈玻,我火速辦了婚禮,結果婚禮上乾颁,老公的妹妹穿的比我還像新娘涂乌。我一直安慰自己,他們只是感情好英岭,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布湾盒。 她就那樣靜靜地躺著,像睡著了一般诅妹。 火紅的嫁衣襯著肌膚如雪罚勾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音荧库,去河邊找鬼堰塌。 笑死,一個胖子當著我的面吹牛分衫,可吹牛的內(nèi)容都是我干的场刑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚪战,長吁一口氣:“原來是場噩夢啊……” “哼牵现!你這毒婦竟也來了?” 一聲冷哼從身側響起邀桑,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瞎疼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后壁畸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贼急,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年捏萍,在試婚紗的時候發(fā)現(xiàn)自己被綠了太抓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡令杈,死狀恐怖走敌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逗噩,我是刑警寧澤掉丽,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站异雁,受9級特大地震影響捶障,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜片迅,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一残邀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柑蛇,春花似錦芥挣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盆耽,卻和暖如春蹋砚,著一層夾襖步出監(jiān)牢的瞬間扼菠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工坝咐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留循榆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓墨坚,卻偏偏與公主長得像秧饮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子泽篮,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容