音視頻學習 (三) JNI 從入門到掌握

前言

音視頻系列文章已經發布 2 篇了,C/C++ 基礎我們也已經學完了,那麼該篇文章開始就真正進入 NDK 學習了,在進入 NDK 學習以前咱們還要學習 JNI 基礎。爲了保證該系列文章輸出,之後儘可能一週一篇。html

介紹

JNI 是 Java 程序設計語言功能功能最強的特徵,它容許 Java 類的某些方法原生實現,同時讓它們可以像普通 Java 方法同樣被調用和使用。這些原生方法也可使用 Java 對象,使用方法與 Java 代碼調用 Java 對象的方法相同。原生方法能夠建立新的 Java 對象或者使用 Java 應用程序建立的對象,這些 Java 應用程序能夠檢查、修改和調用這些對象的方法以執行任務。java

環境配置

安裝 AS + NDK + CMake + LLDBandroid

  • AS: Android 開發工具。
  • NDK:這套工具集容許爲 Android 使用 C 和 C++ 代碼。
  • CMake:一款外部構建工具,可與 Gradle 搭配使用來構建原生庫。若是隻計劃使用 ndk-build,則不須要此組件。
  • LLDB:debug 調式。

local.properties 配置:ios

ndk.dir=/Users/devyk/Data/Android/SDK/ndk-bundle
sdk.dir=/Users/devyk/Data/Android/SDK
複製代碼

build.gradle 配置:c++

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 代碼:bash

    #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++ 類型 size 位
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 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 objects jobjects
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 文檔 或者 JNI 方法大全及使用示例

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 使用手冊

相關文章
相關標籤/搜索