## 音視頻學習 (三) JNI 從入門到掌握
### 前言
音視頻系列文章已經發布 2 篇了,C/C++ 基礎咱們也已經學完了,那么該篇文章開始就真正進入 NDK 學習了,在進入 NDK 學習之前我們還要學習 JNI 基礎。為了保證該系列文章輸出,以后盡量一周一篇。
### 介紹
JNI 是 Java 程序設計語言功能功能最強的特征,它允許 Java 類的某些方法原生實現,同時讓它們能夠像普通 Java 方法一樣被調用和使用。這些原生方法也可以使用 Java 對象,使用方法與 Java 代碼調用 Java 對象的方法相同。原生方法可以創建新的 Java 對象或者使用 Java 應用程序創建的對象,這些 Java 應用程序可以檢查、修改和調用這些對象的方法以執行任務。
### 環境配置
安裝 AS + NDK + CMake + LLDB

* AS: Android 開發工具。
* NDK:這套工具集允許為 Android 使用 C 和 C++ 代碼。
* CMake:一款外部構建工具,可與 Gradle 搭配使用來構建原生庫。如果只計劃使用 ndk-build,則不需要此組件。
* LLDB:debug 調式。
**local.properties 配置:**
~~~
ndk.dir=/Users/devyk/Data/Android/SDK/ndk-bundle
sdk.dir=/Users/devyk/Data/Android/SDK
~~~
**build.gradle 配置:**
~~~
android {
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
~~~
### 簡單示例
1. 創建 native c++ 工程

根據提示點擊 next
2. 基本代碼生成
~~~
public class MainActivity extends AppCompatActivity {
/**
* 1. 加載 native 庫
*/
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
/**3.調用 native c++ 函數*/
tv.setText(stringFromJNI());
}
/**
* 2. 定義 native 函數
*/
public native String stringFromJNI();
}
~~~
Native-lib.cpp 代碼:
~~~
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_devyk_ndk_1sample_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
~~~
運行之后屏幕就會出現 "Hello from C++" 字符串,一個最簡單的 native 項目就創建完成了。
### JNI 入門學習
#### 1. 數據類型和類型描述符
Java 中有兩種數據類型:
* 基本數據類型: boolean 、char、byte、int、short、long、float、double。
* 引用數據類型: String、Object\[\]、Class、Object 及其它類。
**1.1 基本數據類型**
基本數據類型可以直接與 C/C++ 的相應基本數據類型映射,如下表所示。JNI 用類型定義使得這種映射對開發人員透明。
| Java 類型 | JNI 類型 | C/C++ 類型 |
| --- | --- | --- |
| boolean | jboolean | unsigned char (無符號 8 位整型) |
| byte | jbyte | char (有符號 8 位整型) |
| char | jchar | unsingned short (無符號 16 位整型) |
| short | jshort | short (有符號 16 位整型) |
| int | jint | int (有符號 32 位整型) |
| long | jlong | long (有符號 64 位整型) |
| float | jfloat | float (有符號 32 位浮點型) |
| double | jdouble | double (有符號 64 位雙精度型) |
**1.2 引用類型:**
與基本數據類型不同,引用類型對原生方法時不透明的,引用類型映射如下表所示。它們的內部數據結構并不直接向原生代碼公開。
| Java 類型 | 原生類型 |
| --- | --- |
| Java.lang.Class | jclass |
| Java.lang.Throwable | jthrowable |
| Java.lang.String | jstring |
| Other object | jobject |
| Java.lang.Object\[\] | jobjectArray |
| boolean\[\] | jbooleanArray |
| byte\[\] | jbyteArray |
| char\[\] | jcharArray |
| short\[\] | jshortArray |
| int\[\] | jintArray |
| long\[\] | jlongArray |
| float\[\] | jfloatArray |
| double\[\] | jdoubleArray |
| Other arrays | jarray |
**1.3 數據類型描述符**
在 JVM 虛擬機中,存儲數據類型的名稱時,是使用指定的描述符來存儲,而不是我們習慣的 int,float 等。
| Java 類型 | 簽名 (描述符) |
| --- | --- |
| boolean | Z |
| byte | B |
| char | C |
| short | S |
| int | I |
| long | J |
| float | F |
| double | D |
| void | V |
| 其它引用類型 | L + 全類名 + ; |
| type\[\] | \[ |
| method type | (參數)返回值 |
示例:
> * 表示一個 String
>
> Java 類型 : java.lang.String
>
> JNI 描述符: Ljava/lang/String; (L + 類全名 + ;)
>
> * 表示一個數組
>
> Java 類型: String\[\] JNI 描述符: \[Ljava/lang/String; Java 類型: int \[\] \[\] JNI 描述符: \[\[I
>
> * 表示一個方法
>
> Java 方法: long func(int n, String s, int\[\] arr); JNI 描述符: (ILjava/lang/String;\[I)J
>
> Java 方法: void func(); JNI 描述符: ()V
也可以使用命令 :**javap -s 全路徑**來獲取方法簽名

#### 2. JNIEnv 和 JavaVm 介紹
**2.1 JNIEnv :**
JNIEnv 表示 Java 調用 native 語言的環境,是一個封裝了幾乎全部 JNI 方法的指針。
JNIEnv 只在創建它的線程生效,不能跨線程傳遞,不同線程的 JNIEnv 彼此獨立。
native 環境中創建的線程,如果需要訪問 JNI,必須要調用 AttachCurrentThread 關聯,并使用 DetachCurrentThread 解除鏈接。
**2.2 JavaVm :**
JavaVM 是虛擬機在 JNI 層的代表,**一個進程只有一個 JavaVM**,所有的線程共用一個 JavaVM。
**2.3 代碼風格 (C/C++)**
~~~
C: (*env)->NewStringUTF(env, “Hellow World!”);
C++: env->NewStringUTF(“Hellow World!”);
復制代碼
~~~
#### 3. JNI API
參考[官方 API 文檔](https://docs.oracle.com/javase/10/docs/specs/jni/index.html)或者[JNI 方法大全及使用示例](https://blog.csdn.net/afei__/article/details/81016413)
#### 4. 對數據類型的操作
**JNI 處理 Java 傳遞過來的數據**
1. 定義 native 函數
~~~
public class MainActivity extends AppCompatActivity {
/**
* 1. 加載 native 庫
*/
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/** 1. Java 數據傳遞給 native */
test1(true,
(byte) 1,
',',
(short) 3,
4,
3.3f,
2.2d,
"DevYK",
28,
new int[]{1, 2, 3, 4, 5, 6, 7},
new String[]{"1", "2", "4"},
new Person("陽坤"),
new boolean[]{false, true}
);
}
/**
* Java 將數據傳遞到 native 中
*/
public native void test1(
boolean b,
byte b1,
char c,
short s,
long l,
float f,
double d,
String name,
int age,
int[] i,
String[] strs,
Person person,
boolean[] bArray
);
}
復制代碼
~~~
2. jni 處理 Java 傳遞過來的數據
~~~
#include <jni.h>
#include <string>
#include <android/log.h>
#include <iostream>
#define TAG "native-lib"
// __VA_ARGS__ 代表 ...的可變參數
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
extern "C"//支持 C 語言代碼
JNIEXPORT void JNICALL
Java_com_devyk_ndk_1sample_MainActivity_test1(JNIEnv *env, jobject instance,
jboolean jboolean1,
jbyte jbyte1,
jchar jchar1,
jshort jshort1,
jlong jlong1,
jfloat jfloat1,
jdouble jdouble1,
jstring name_,
jint age,
jintArray i_,
jobjectArray strs,
jobject person,
jbooleanArray bArray_
) {
//1. 接收 Java 傳遞過來的 boolean 值
unsigned char b_boolean = jboolean1;
LOGD("boolean-> %d", b_boolean);
//2. 接收 Java 傳遞過來的 boolean 值
char c_byte = jbyte1;
LOGD("jbyte-> %d", c_byte);
//3. 接收 Java 傳遞過來的 char 值
unsigned short c_char = jchar1;
LOGD("char-> %d", c_char);
//4. 接收 Java 傳遞過來的 short 值
short s_short = jshort1;
LOGD("short-> %d", s_short);
//5. 接收 Java 傳遞過來的 long 值
long l_long = jlong1;
LOGD("long-> %d", l_long);
//6. 接收 Java 傳遞過來的 float 值
float f_float = jfloat1;
LOGD("float-> %f", f_float);
//7. 接收 Java 傳遞過來的 double 值
double d_double = jdouble1;
LOGD("double-> %f", d_double);
//8. 接收 Java 傳遞過來的 String 值
const char *name_string = env->GetStringUTFChars(name_, 0);
LOGD("string-> %s", name_string);
//9. 接收 Java 傳遞過來的 int 值
int age_java = age;
LOGD("int:%d", age_java);
//10. 打印 Java 傳遞過來的 int []
jint *intArray = env->GetIntArrayElements(i_, NULL);
//拿到數組長度
jsize intArraySize = env->GetArrayLength(i_);
for (int i = 0; i < intArraySize; ++i) {
LOGD("intArray->%d:", intArray[i]);
}
//釋放數組
env->ReleaseIntArrayElements(i_, intArray, 0);
//11. 打印 Java 傳遞過來的 String[]
jsize stringArrayLength = env->GetArrayLength(strs);
for (int i = 0; i < stringArrayLength; ++i) {
jobject jobject1 = env->GetObjectArrayElement(strs, i);
//強轉 JNI String
jstring stringArrayData = static_cast<jstring >(jobject1);
//轉 C String
const char *itemStr = env->GetStringUTFChars(stringArrayData, NULL);
LOGD("String[%d]: %s", i, itemStr);
//回收 String[]
env->ReleaseStringUTFChars(stringArrayData, itemStr);
}
//12. 打印 Java 傳遞過來的 Object 對象
//12.1 獲取字節碼
const char *person_class_str = "com/devyk/ndk_sample/Person";
//12.2 轉 jni jclass
jclass person_class = env->FindClass(person_class_str);
//12.3 拿到方法簽名 javap -a
const char *sig = "()Ljava/lang/String;";
jmethodID jmethodID1 = env->GetMethodID(person_class, "getName", sig);
jobject obj_string = env->CallObjectMethod(person, jmethodID1);
jstring perStr = static_cast<jstring >(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD("Person: %s", itemStr2);
env->DeleteLocalRef(person_class); // 回收
env->DeleteLocalRef(person); // 回收
//13. 打印 Java 傳遞過來的 booleanArray
jsize booArrayLength = env->GetArrayLength(bArray_);
jboolean *bArray = env->GetBooleanArrayElements(bArray_, NULL);
for (int i = 0; i < booArrayLength; ++i) {
bool b = bArray[i];
jboolean b2 = bArray[i];
LOGD("boolean:%d",b)
LOGD("jboolean:%d",b2)
}
//回收
env->ReleaseBooleanArrayElements(bArray_, bArray, 0);
}
復制代碼
~~~
輸出:
~~~
> **輸出:**
>
> native-lib: boolean-> 1
> native-lib: jbyte-> 1
> native-lib: char-> 44
> native-lib: short-> 3
> native-lib: long-> 4
> native-lib: float-> 3.300000
> native-lib: double-> 2.200000
> native-lib: string-> DevYK
> native-lib: int:28
> native-lib: intArray->1:
> native-lib: intArray->2:
> native-lib: intArray->3:
> native-lib: intArray->4:
> native-lib: intArray->5:
> native-lib: intArray->6:
> native-lib: intArray->7:
> native-lib: String[0]: 1
> native-lib: String[1]: 2
> native-lib: String[2]: 4
> native-lib: Person: 陽坤
> native-lib: boolean:0
> native-lib: jboolean:0
> native-lib: boolean:1
> native-lib: jboolean:1
復制代碼
~~~
**JNI 處理 Java 對象**
1. 定義一個 Java 對象
~~~
public class Person {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
復制代碼
~~~
2. 定義 native 接口
~~~
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
* 1. 加載 native 庫
*/
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView text = findViewById(R.id.sample_text);
/**處理 Java 對象*/
String str = getPerson().toString();
text.setText(str);
}
public native Person getPerson();
}
復制代碼
~~~
根據上面代碼我們知道,如果獲取成功,手機屏幕上肯定會打印會顯示數據。
3. JNI 的處理
~~~
extern "C"
JNIEXPORT jobject JNICALL
Java_com_devyk_ndk_1sample_MainActivity_getPerson(JNIEnv *env, jobject instance) {
//1. 拿到 Java 類的全路徑
const char *person_java = "com/devyk/ndk_sample/Person";
const char *method = "<init>"; // Java構造方法的標識
//2. 找到需要處理的 Java 對象 class
jclass j_person_class = env->FindClass(person_java);
//3. 拿到空參構造方法
jmethodID person_constructor = env->GetMethodID(j_person_class, method, "()V");
//4. 創建對象
jobject person_obj = env->NewObject(j_person_class, person_constructor);
//5. 拿到 setName 方法的簽名,并拿到對應的 setName 方法
const char *nameSig = "(Ljava/lang/String;)V";
jmethodID nameMethodId = env->GetMethodID(j_person_class, "setName", nameSig);
//6. 拿到 setAge 方法的簽名,并拿到 setAge 方法
const char *ageSig = "(I)V";
jmethodID ageMethodId = env->GetMethodID(j_person_class, "setAge", ageSig);
//7. 正在調用 Java 對象函數
const char *name = "DevYK";
jstring newStringName = env->NewStringUTF(name);
env->CallVoidMethod(person_obj, nameMethodId, newStringName);
env->CallVoidMethod(person_obj, ageMethodId, 28);
const char *sig = "()Ljava/lang/String;";
jmethodID jtoString = env->GetMethodID(j_person_class, "toString", sig);
jobject obj_string = env->CallObjectMethod(person_obj, jtoString);
jstring perStr = static_cast<jstring >(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD("Person: %s", itemStr2);
return person_obj;
}
復制代碼
~~~
輸出:

可以看到 native 返回數據給 Java 了。
### 5\. JNI 動態注冊
前面咱們學習的都是靜態注冊,靜態注冊雖然簡單方便,但是也面臨一個較大的問題,如果當前類定義的 native 方法名稱改變或者包名改變,那么這一改也就面臨在 cpp 中實現的也將改動,如果將要面臨這種情況你可以試試 JNI 動態注冊,如下代碼所示:
~~~
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
* 1. 加載 native 庫
*/
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView text = findViewById(R.id.sample_text);
/**動態注冊的 native */
dynamicRegister("我是動態注冊的");
}
/**
* 動態注冊
*/
public native void dynamicRegister(String name);
}
復制代碼
~~~
cpp:
~~~
#include <jni.h>
#include <string>
#include <android/log.h>
#include <iostream>
#define TAG "native-lib"
// __VA_ARGS__ 代表 ...的可變參數
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
/**
* TODO 動態注冊
*/
/**
* 對應java類的全路徑名,.用/代替
*/
const char *classPathName = "com/devyk/ndk_sample/MainActivity";
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_dynamicRegister(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD("動態注冊: %s", j_name)
//釋放
env->ReleaseStringUTFChars(name, j_name);
}
/* 源碼結構體
* typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
static const JNINativeMethod jniNativeMethod[] = {
{"dynamicRegister", "(Ljava/lang/String;)V", (void *) (native_dynamicRegister)}
};
/**
* 該函數定義在jni.h頭文件中,System.loadLibrary()時會調用JNI_OnLoad()函數
*/
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *javaVm, void *pVoid) {
//通過虛擬機 創建愛你全新的 evn
JNIEnv *jniEnv = nullptr;
jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
if (result != JNI_OK) {
return JNI_ERR; // 主動報錯
}
jclass mainActivityClass = jniEnv->FindClass(classPathName);
jniEnv->RegisterNatives(mainActivityClass, jniNativeMethod,
sizeof(jniNativeMethod) / sizeof(JNINativeMethod));//動態注冊的數量
return JNI_VERSION_1_6;
}
復制代碼
~~~
> **輸出:**
>
> 動態注冊: 我是動態注冊的
### 6\. 異常處理
異常處理是 Java 程序設計語言的重要功能, JNI 中的異常行為與 Java 中的有所不同,在 Java 中,當拋出一個異常時,虛擬機停止執行代碼塊并進入調用棧反向檢查能處理特定類型異常的異常處理程序代碼塊,這也叫捕獲異常。虛擬機清除異常并將控制權交給異常處理程序。相比之下, JNI 要求開發人員在異常發生后顯式地實現異常處理流。
**捕獲異常:**
JNIEvn 接口提供了一組與異常相關的函數集,在運行過程中可以使用 Java 類查看這些函數,比如代碼如下:
~~~
public native void dynamicRegister2(String name);
/**
* 測試拋出異常
*
* @throws NullPointerException
*/
private void testException() throws NullPointerException {
throw new NullPointerException("MainActivity testException NullPointerException");
}
復制代碼
~~~
當調用 testException 方法時,dynamicRegister2 該原生方法需要顯式的處理異常信息,JNI 提供了 ExceptionOccurred 函數查詢虛擬機中是否有掛起的異常。在使用完之后,異常處理程序需要用 ExceptionClear 函數顯式的清除異常,如下代碼:
~~~
jthrowable exc = env->ExceptionOccurred(); // 檢測是否發生異常
if (exc) {//如果發生異常
env->ExceptionDescribe(); // 打印異常信息
env->ExceptionClear(); // 清除掉發生的異常
}
復制代碼
~~~
**拋出異常:**
JNI 也允許原生代碼拋出異常。因為異常是 Java 類,應該先用 FindClass 函數找到異常類,用 ThrowNew 函數可以使用化且拋出新的異常,如下代碼所示:
~~~
jthrowable exc = env->ExceptionOccurred(); // 檢測是否發生異常
if (exc) {//如果發生異常
jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(newExcCls, "JNI 中發生了一個異常信息"); // 返回一個新的異常到 Java
}
復制代碼
~~~
因為原生函數的代碼執行不受虛擬機的控制,因此拋出異常并不會停止原生函數的執行并把控制權交給異常處理程序。到拋出異常時,原生函數應該釋放所有已分配的原生資源,例如內存及合適的返回值等。通過 JNIEvn 接口獲得的引用是局部引用且一旦返回原生函數,它們自動地被虛擬機釋放。
**示例代碼:**
~~~
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
* 1. 加載 native 庫
*/
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dynamicRegister2("測試異常處理");
}
public native void dynamicRegister2(String name);
/**
* 測試拋出異常
*
* @throws NullPointerException
*/
private void testException() throws NullPointerException {
throw new NullPointerException("MainActivity testException NullPointerException");
}
}
復制代碼
~~~
native-lib.cpp 文件
~~~
#include <jni.h>
#include <string>
#include <android/log.h>
#include <iostream>
#define TAG "native-lib"
// __VA_ARGS__ 代表 ...的可變參數
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
/**
* TODO 動態注冊
*/
....
...
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_dynamicRegister2(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD("動態注冊: %s", j_name)
jclass clazz = env->GetObjectClass(instance);//拿到當前類的class
jmethodID mid =env->GetMethodID(clazz, "testException", "()V");//執行 Java 測試拋出異常的代碼
env->CallVoidMethod(instance, mid); // 執行會拋出一個異常
jthrowable exc = env->ExceptionOccurred(); // 檢測是否發生異常
if (exc) {//如果發生異常
env->ExceptionDescribe(); // 打印異常信息
env->ExceptionClear(); // 清除掉發生的異常
jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(newExcCls, "JNI 中發生了一個異常信息"); // 返回一個新的異常到 Java
}
//釋放
env->ReleaseStringUTFChars(name, j_name);
}
...
復制代碼
~~~
這里還是使用的動態注冊。
最后效果如下:

可以看見這里即捕獲到了 Java 拋出的異常,也拋出了一個 JNI 新的異常信息。
### 7\. 局部與全局引用
引用在 Java 程序設計中扮演非常重要的角色。虛擬機通過追蹤類實例的引用并收回不在引用的垃圾來管理類實例的使用期限。因為原生代碼不是一個管理環境,因此 JNI 提供了一組函數允許原生代碼顯式地管理對象引用及使用期間原生代碼。 JNI 支持三種引用: 局部引用、全局引用和弱全局引用。下面將介紹這幾類引用。
**局部引用:**
大多數 JNI 函數返回局部引用。局部應用不能在后續的調用中被緩存及重用,主要是因為它們的使用期限僅限于原生方法,一旦原生方法返回,局部引用即被釋放。例如: FindClass 函數返回一個局部引用,當原生方法返回時,它被自動釋放,也可以用 DeleteLocalRef 函數顯式的釋放原生代碼。如下代碼所示:
~~~
jclass personClass;
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_test4(JNIEnv *env, jobject instance) {
LOGD("測試局部引用")
if (personClass == NULL) {
const char *person_class = "com/devyk/ndk_sample/Person";
personClass = env->FindClass(person_class);
LOGD("personClass == null 執行了。")
}
//Java Person 構造方法實例化
const char *sig = "()V";
const char *method = "<init>";//Java 構造方法標識
jmethodID init = env->GetMethodID(personClass, method, sig);
//創建出來
env->NewObject(personClass, init);
}
復制代碼
~~~
效果:

跟介紹的一樣的吧。局部引用不能再后續的調用中重復使用,那么怎么解決這個問題勒,其實只要把局部引用提升為全局引用或者調用 DeleteLocalRef 顯式釋放就行了。這個我們在全局引用中演示。
**全局引用:**
全局引用在原生方法的后續調用過程中依然有效,除非它們被原生代碼顯式釋放。
1. 創建全局引用
可以使用 NewGlobalRef 函數將局部引用初始化為全局引用,如下代碼所示:
~~~
jclass personClass;
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_test4(JNIEnv *env, jobject instance) {
LOGD("測試局部引用")
if (personClass == NULL) {
//1. 提升全局解決不能重復使用問題
const char *person_class = "com/devyk/ndk_sample/Person";
jclass jclass1 = env->FindClass(person_class);
personClass = static_cast<jclass>(env->NewGlobalRef(jclass1));
LOGD("personClass == null 執行了。")
}
//Java Person 構造方法實例化
const char *sig = "()V";
const char *method = "<init>";//Java 構造方法標識
jmethodID init = env->GetMethodID(personClass, method, sig);
//創建出來
env->NewObject(personClass, init);
//2. 顯式釋放主動刪除全局引用
env->DeleteLocalRef(personClass);
personClass = NULL;
}
復制代碼
~~~

2. 刪除全局引用
當原生代碼不再需要一個全局引用時,可以隨時用 DeleteGlobalRef 函數釋放它,如下代碼所示:
~~~
env->DeleteLocalRef(personClass);
personClass = NULL;
復制代碼
~~~
**弱全局引用**
全局引用的另一種類型是弱全局引用。與全局引用一樣,弱全局引用在原生方法的后續調用依然有效。與全局引用不同,弱全局引用并不阻止潛在的對象被垃圾收回。
1. 創建弱全局引用
可以使用 NewWeakGlobalRef 函數對弱全局引用進行初始化,如下所示:
~~~
jclass personClass;
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_test4(JNIEnv *env, jobject instance) {
LOGD("測試局部引用")
if (personClass == NULL) {
//1. 提升全局解決不能重復使用問題
const char *person_class = "com/devyk/ndk_sample/Person";
jclass jclass1 = env->FindClass(person_class);
// personClass = static_cast<jclass>(env->NewGlobalRef(jclass1));
personClass = static_cast<jclass>(env->NewWeakGlobalRef(jclass1));
LOGD("personClass == null 執行了。")
}
//Java Person 構造方法實例化
const char *sig = "()V";
const char *method = "<init>";//Java 構造方法標識
jmethodID init = env->GetMethodID(personClass, method, sig);
//創建出來
env->NewObject(personClass, init);
//2. 顯式釋放主動刪除局部引用
// env->DeleteLocalRef(personClass);
env->DeleteWeakGlobalRef(personClass);
personClass = NULL;
}
復制代碼
~~~
2. 弱全局引用的有效性檢驗
可以用 IsSameObject 函數來檢驗一個弱全局引用是否仍然指向活動的類實例.
3. 刪除弱全局引用
~~~
env->DeleteWeakGlobalRef(personClass);
復制代碼
~~~
全局引用顯示釋放前一直有效,它們可以被其它原生函數及原生線程使用。
### 8\. JNI 線程操作
作為多線程環境的一部分,虛擬機支持運行的原生代碼。在開發構件時要記住 JNI 技術的一些約束:
* 只有再原生方法執行期間及正在執行原生方法的線程環境下局部引用是有效的,局部引用不能再多線程間共享,只有全局可以被多個線程共享。
* 被傳遞給每個原生方法的 JNIEvn 接口指針在與方法調用相關的線程中也是有效的,它不能被其它線程緩存或使用。
**同步:**
同步是多線程程序設計最終的特征。與 Java 同步類似, JNI 的監視器允許原生代碼利用 Java 對象同步,虛擬機保證存取監視器的線程能夠安全執行,而其他線程等待監視器對象變成可用狀態。
~~~
jint MonitorEnter(jobject obj)
復制代碼
~~~
對 MonitorEnter 函數的調用應該與對 MonitorExit 的調用相匹配,從而避免代碼出現死鎖。
例子:
~~~
public void test4(View view) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
count();
nativeCount();
}
}).start();
}
}
private void count() {
synchronized (this) {
count++;
Log.d("Java", "count=" + count);
}
}
public native void nativeCount();
復制代碼
~~~
native 代碼:
~~~
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_count(JNIEnv *env, jobject instance) {
jclass cls = env->GetObjectClass(instance);
jfieldID fieldID = env->GetFieldID(cls, "count", "I");
/*if (env->MonitorEnter(instance) != JNI_OK) {
LOGE("%s: MonitorEnter() failed", __FUNCTION__);
}*/
/* synchronized block */
int val = env->GetIntField(instance, fieldID);
val++;
LOGI("count=%d", val);
env->SetIntField(instance, fieldID, val);
/*if (env->ExceptionOccurred()) {
LOGE("ExceptionOccurred()...");
if (env->MonitorExit(instance) != JNI_OK) {
LOGE("%s: MonitorExit() failed", __FUNCTION__);
};
}
if (env->MonitorExit(instance) != JNI_OK) {
LOGE("%s: MonitorExit() failed", __FUNCTION__);
};*/
}
復制代碼
~~~
在 native 中沒有進行同步,打印如下:
~~~
> **輸出:**
>
> com.devyk.ndk_sample D/Java: count=1
> com.devyk.ndk_sample I/native-lib: count=2
> com.devyk.ndk_sample D/Java: count=3
> com.devyk.ndk_sample I/native-lib: count=4
> com.devyk.ndk_sample D/Java: count=5
> com.devyk.ndk_sample I/native-lib: count=6
> com.devyk.ndk_sample D/Java: count=7
> com.devyk.ndk_sample I/native-lib: count=8
> com.devyk.ndk_sample D/Java: count=9
> com.devyk.ndk_sample I/native-lib: count=10
> com.devyk.ndk_sample D/Java: count=11
> com.devyk.ndk_sample I/native-lib: count=12
> com.devyk.ndk_sample D/Java: count=13
> com.devyk.ndk_sample I/native-lib: count=15
> com.devyk.ndk_sample D/Java: count=15
> com.devyk.ndk_sample I/native-lib: count=16
> com.devyk.ndk_sample D/Java: count=17
> com.devyk.ndk_sample I/native-lib: count=18
> com.devyk.ndk_sample D/Java: count=19
> com.devyk.ndk_sample I/native-lib: count=20
復制代碼
~~~
通過多線程對 count 字段操作,可以看見已經無法保證 count 的可見性了。這就需要 JNI 本地實現也要同步。
我們把注釋放開:
打印如下:
~~~
> **輸出:**
>
> com.devyk.ndk_sample D/Java: count=1
> com.devyk.ndk_sample I/native-lib: count=2
> com.devyk.ndk_sample D/Java: count=3
> com.devyk.ndk_sample I/native-lib: count=4
> com.devyk.ndk_sample D/Java: count=5
> com.devyk.ndk_sample I/native-lib: count=6
> com.devyk.ndk_sample D/Java: count=7
> com.devyk.ndk_sample I/native-lib: count=8
> com.devyk.ndk_sample D/Java: count=9
> com.devyk.ndk_sample I/native-lib: count=10
> com.devyk.ndk_sample D/Java: count=11
> com.devyk.ndk_sample I/native-lib: count=12
> com.devyk.ndk_sample D/Java: count=13
> com.devyk.ndk_sample D/Java: count=14
> com.devyk.ndk_sample I/native-lib: count=15
> com.devyk.ndk_sample I/native-lib: count=16
> com.devyk.ndk_sample D/Java: count=17
> com.devyk.ndk_sample I/native-lib: count=18
> com.devyk.ndk_sample D/Java: count=19
> com.devyk.ndk_sample I/native-lib: count=20
復制代碼
~~~
現在保證了count 的可見性了。
**原生線程:**
為了執行特定任務,這些原生構建可以并行使用原生線程。因為虛擬機不知道原生線程,因此它們不能與 Java 構建直接通信。為了與應用的依然活躍部分交互,原生線程應該先附著在虛擬機上。
JNI 通過 JavaVM 接口指針提供了 AttachCurrentThread 函數以便于讓原生代碼將原生線程附著到虛擬機上,如下代碼所示, JavaVM 接口指針應該盡早被緩存,否則的話它不能被獲取。
~~~
JavaVM* jvm;
...
JNIEnv* env = NULL;
...
jvm->AttachCurrentThread(&env,0);//把 native 線程附著到 JVM 上
...
jvm->DetachCurrentThread();//解除 附著 到 JVM 的 native 線程
復制代碼
~~~
對 AttachCurrentThread 函數的調用允許應用程序獲得對當前線程有效的 JNIEnv 接口指針。將一個已經附著的原生線程再次附著不會有任何副作用。當原生線程完成時,可以用 DetachCurrentThread 函數將原生線程與虛擬機分離。
例子:
MainActivity.java
~~~
public void test5(View view) {
testThread();
}
// AndroidUI操作,讓C++線程里面來調用
public void updateUI() {
if (Looper.getMainLooper() == Looper.myLooper()) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("UI")
.setMessage("native 運行在主線程,直接更新 UI ...")
.setPositiveButton("確認", null)
.show();
} else {
runOnUiThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("UI")
.setMessage("native運行在子線程切換為主線程更新 UI ...")
.setPositiveButton("確認", null)
.show();
}
});
}
}
public native void testThread();
public native void unThread();
@Override
protected void onDestroy() {
super.onDestroy();
unThread();
}
復制代碼
~~~
native-lib.cpp
~~~
JavaVM * jvm;
jobject instance;
void * customThread(void * pVoid) {
// 調用的話,一定需要JNIEnv *env
// JNIEnv *env 無法跨越線程,只有JavaVM才能跨越線程
JNIEnv * env = NULL; // 全新的env
int result = jvm->AttachCurrentThread(&env, 0); // 把native的線程,附加到JVM
if (result != 0) {
return 0;
}
jclass mainActivityClass = env->GetObjectClass(instance);
// 拿到MainActivity的updateUI
const char * sig = "()V";
jmethodID updateUI = env->GetMethodID(mainActivityClass, "updateUI", sig);
env->CallVoidMethod(instance, updateUI);
// 解除 附加 到 JVM 的native線程
jvm->DetachCurrentThread();
return 0;
}
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_testThread(JNIEnv *env, jobject thiz) {
instance = env->NewGlobalRef(thiz); // 全局的,就不會被釋放,所以可以在線程里面用
// 如果是非全局的,函數一結束,就被釋放了
pthread_t pthreadID;
pthread_create(&pthreadID, 0, customThread, instance);
pthread_join(pthreadID, 0);
}
extern "C" //支持 C 語言
JNIEXPORT void JNICALL //告訴虛擬機,這是jni函數
native_unThread(JNIEnv *env, jobject thiz) {
if (NULL != instance) {
env->DeleteGlobalRef(instance);
instance = NULL;
}
}
~~~
效果:

## 總結
該篇文件全面介紹了 JNI 技術實現 Java 應用程序與原生代碼之間通信的方式,關于更多 JNI 技術可以下載[JNI 使用手冊](https://pan.baidu.com/s/1HudsXKOghlmHEcMMlp1W6g)。
- 前言
- JNI基礎知識
- C語言知識點總結
- ①基本語法
- ②數據類型
- 枚舉類型
- 自定義類型(類型定義)
- ③格式化輸入輸出
- printf函數
- scanf函數
- 編程規范
- ④變量和常量
- 局部變量和外部變量
- ⑤類型轉換
- ⑥運算符
- ⑦結構語句
- 1、分支結構(選擇語句)
- 2、循環結構
- 退出循環
- break語句
- continue語句
- goto語句
- ⑧函數
- 函數的定義和調用
- 參數
- 函數的返回值
- 遞歸函數
- 零起點學通C語言摘要
- 內部函數和外部函數
- 變量存儲類別
- ⑨數組
- 指針
- 結構體
- 聯合體(共用體)
- 預處理器
- 預處理器的工作原理
- 預處理指令
- 宏定義
- 簡單的宏
- 帶參數的宏
- 預定義宏
- 文件包含
- 條件編譯
- 內存中的數據
- C語言讀文件和寫文件
- JNI知識點總結
- 前情回顧
- JNI規范
- jni開發
- jni開發中常見的錯誤
- JNI實戰演練
- C++(CPP)在Android開發中的應用
- 掘金網友總結的音視頻開發知識
- 音視頻學習一、C 語言入門
- 1.程序結構
- 2. 基本語法
- 3. 數據類型
- 4. 變量
- 5. 常量
- 6. 存儲類型關鍵字
- 7. 運算符
- 8. 判斷
- 9. 循環
- 10. 函數
- 11. 作用域規則
- 12. 數組
- 13. 枚舉
- 14. 指針
- 15. 函數指針與回調函數
- 16. 字符串
- 17. 結構體
- 18. 共用體
- 19. typedef
- 20. 輸入 & 輸出
- 21.文件讀寫
- 22. 預處理器
- 23.頭文件
- 24. 強制類型轉換
- 25. 錯誤處理
- 26. 遞歸
- 27. 可變參數
- 28. 內存管理
- 29. 命令行參數
- 總結
- 音視頻學習二 、C++ 語言入門
- 1. 基本語法
- 2. C++ 關鍵字
- 3. 數據類型
- 4. 變量類型
- 5. 變量作用域
- 6. 常量
- 7. 修飾符類型
- 8. 存儲類
- 9. 運算符
- 10. 循環
- 11. 判斷
- 12. 函數
- 13. 數學運算
- 14. 數組
- 15. 字符串
- 16. 指針
- 17. 引用
- 18. 日期 & 時間
- 19. 輸入輸出
- 20. 數據結構
- 21. 類 & 對象
- 22. 繼承
- 23. 重載運算符和重載函數
- 24. 多態
- 25. 數據封裝
- 26. 接口(抽象類)
- 27. 文件和流
- 28. 異常處理
- 29. 動態內存
- 30. 命名空間
- 31. 預處理器
- 32. 多線程
- 總結
- 音視頻學習 (三) JNI 從入門到掌握
- 音視頻學習 (四) 交叉編譯動態庫、靜態庫的入門學習
- 音視頻學習 (五) Shell 腳本入門
- 音視頻學習 (六) 一鍵編譯 32/64 位 FFmpeg 4.2.2
- 音視頻學習 (七) 掌握音頻基礎知識并使用 AudioTrack、OpenSL ES 渲染 PCM 數據
- 音視頻學習 (八) 掌握視頻基礎知識并使用 OpenGL ES 2.0 渲染 YUV 數據
- 音視頻學習 (九) 從 0 ~ 1 開發一款 Android 端播放器(支持多協議網絡拉流/本地文件)
- 音視頻學習 (十) 基于 Nginx 搭建(rtmp、http)直播服務器
- 音視頻學習 (十一) Android 端實現 rtmp 推流
- 音視頻學習 (十二) 基于 FFmpeg + OpenSLES 實現音頻萬能播放器
- 音視頻學習 (十三) Android 中通過 FFmpeg 命令對音視頻編輯處理(已開源)