天天看點

音視訊開發(十五):JNI與NDK的學習和使用

目錄

  1. 什麼是JNI、NDK?
  2. Java和Native互動流程
  3. 通過AS建立Native CPP簡單的項目
  4. JNI基礎知識介紹
  5. 實作JAVA和Native的互相調用

一、什麼是JNI、NDK?

JNI:Java Native Interface(java本地接口),使得Java與本地語言(

C、CPP)互相調用

NDK:Native Development Kit,是Android的一個工具開發包,幫助開發者快速開發C、CPP動态庫,自動将動态庫打包進入APK。

通過JNI實作Java和Native的互動,在Android上通過NDK實作JNI的功能。

二、Java和Native互動流程

JNI

  1. 在Java類中通過native關鍵字聲明Native方法
  2. javac指令編譯Java類得到class檔案
  3. 通過javah指令(javah -jni class名稱)導出JNI的頭檔案(.h檔案)
  4. 實作native方法
  5. 編譯生成動态庫(.so檔案)
  6. 實作Java和C、CPP的互相調用

NDK

  1. 配置NDK環境、建立Natvie CPP項目)
  2. 在Java類中通過native關鍵字聲明Native方法
  3. 自動生成native方法,實作native方法
  4. 通過ndk-build或者cmake編譯産生動态庫
  5. 實作Java和C、CPP的互相調用
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓

三、通過AS建立Native CPP簡單的項目

1. 如何配置NDK環境

SDK Manager —》SDK Tools中下載下傳選中NDK,LLDB和CMake。

其中NDK是Native開發工具包,

LLDB是調試Native代碼用

CMake是編譯工具

音視訊開發(十五):JNI與NDK的學習和使用

配置好環境後,我們就可以開始建立Native CPP項目了

2. 通過AS建立CPP項目

AS New Project 選中 Native CPP項目。即可自動建立一個demo項目。

Java通過調用native方法stringFromJNI擷取一個字元串。

//Java 代碼
public class MainActivity extends AppCompatActivity {

    ...
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    public native static String stringFromJNIStatic();
}

//JNI 代碼
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_stringFromJNIStatic(JNIEnv *env, jclass clazz) {

}
           

代碼比較簡單,但是麻雀雖小,五髒俱全。我們看到這個小小的JNI方法可能會有以下疑問。

  1. stringFromJNI生成的關聯的Native方法名稱為什麼是Java_com_av_mediajourney_MainActivity_stringFromJNI?可以是其他的嗎?
  2. JNI方法的參數 JNIEnv* 和 jobject代碼什麼意思?*
  3. Java需要的是String類型,為什麼JNI傳回的是一個jstring類型?
  4. extern "C" 是什麼意思?
  5. JNIEXPORT和JNICALL又是什麼意思?

這涉及到JNI的基本知識,我們通過對JNI基本知識的學習來解決上面的疑惑。

四、JNI基本知識

本小節分如下内容

  1. JNIEnv和jobject jclass
  2. Java 語言中的資料類型是如何映射到 c/cpp本地語言中的
  3. java的屬性和方法在JNI 簽名

4.1 JNIEnv 和 jobject 、 jclass

JNIEnv:是指線程上下文環境,每個線程有且隻有一個JNIEnv執行個體

JNIEnv 結構包括 JNI 函數表

音視訊開發(十五):JNI與NDK的學習和使用

第二個參數的意義取決于該方法是靜态還是執行個體方法(static or an instance method)。音視訊開發知識點路線圖:https://docs.qq.com/doc/DQm1VTHBlQmdmTlN2,當本地方法作為一個執行個體方法時,第二個參數相當于對象本身,即 this. 當本地方法作為一個靜态方法時,指向所在類.

4.2 Java 語言中的資料類型是如何映射到 c/cpp本地語言中的

在 Java 中有兩類資料類型:基本資料類型,如,boolean,int, float, char;另一種為引用資料類型,如,類,執行個體,數組。

Java和JNI的基本資料類型映射關系如下

音視訊開發(十五):JNI與NDK的學習和使用
相比基本類型,對象類型的傳遞要複雜很多。Java 層對象作為指針傳遞到 JNI 層,它指向 JavaVM 内部資料結構。使用這種指針的目的是:不希望 JNI 使用者了解 JavaVM 内部資料結構。對引用類型指針所指結構的操作,都要通過 JNI 方法進行,比如,"java.lang.String"對象,JNI 層對應的類型為 jstring,對該 類型 的操作要通過 JNIEnv-> NewStringUTF 進行。

針對字元串對象

通過字元串拼接來展示

//Java代碼新增字元串拼接的native方法

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText(stringFromJNI()+appendString("hengheng","hahah"));
    }

    ...

    //新加字元串拼接方法
    public native String appendString(String str1,String str2);
}


//對應JNI的實作

extern "C"
JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_appendString(JNIEnv *env, jobject thiz, jstring str1,
                                                   jstring str2) {
    //使用對應的 JNI 函數把 jstring 轉成 C/C++字串
    //Unicode 以 16-bits 值編碼;UTF-8 是一種以位元組為機關變長格式的字元編碼,并與 7-bits
    //ASCII 碼相容。UTF-8 字串與 C 字串一樣,以 NULL('\0')做結束符
    //調用 GetStringUTFChars,把一個 Unicode 字串轉成 UTF-8 格式字串
    const char *string1 = env->GetStringUTFChars(str1, NULL);

    //調用該函數會有記憶體配置設定操作,失敗後,該函數傳回 NULL,并抛 OutOfMemoryError 異常。
    if(string1 == NULL){
        return NULL;
    }
    const char *string2 = env->GetStringUTFChars(str2, NULL);
    if(string2 == NULL){
        return NULL;
    }

    //string:string是STL當中的一個容器,對其進行了封裝,是以操作起來非常友善。
    //char*:char *是一個指針,可以指向一個字元串數組,至于這個數組可以在棧上配置設定,也可以在堆上配置設定,堆得話就要你手動釋放了。
    std::string const cc = std::string(string1) + std::string(string2);

    //調用 ReleaseStringUTFChars 釋放 GetStringUTFChars 中配置設定的記憶體(Unicode -> UTF-8轉換的原因)。
    env->ReleaseStringUTFChars(str1,string1);
    env->ReleaseStringUTFChars(str2,string2);

    //使用 JNIEnv->NewStringUTF 構造 java.lang.String;
    return env->NewStringUTF(cc.c_str());

}
           
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓

JNI String函數彙總表如下:

音視訊開發(十五):JNI與NDK的學習和使用

針對數組

通過求和int類型數組來展示 

//java代碼修改
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        int[] intarry = new int[10];
        for (int i = 0; i < 10; i++) {
            intarry[i] = i;
        }
        tv.setText(stringFromJNI()+appendString("hengheng","hahah")+sumArray(intarry));
    }

    ...

    public native int sumArray(int[] intarray);


}


//JNI實作

extern "C"
JNIEXPORT jint JNICALL
Java_com_av_mediajourney_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray intarray) {

    jint sum;
    jsize length = env->GetArrayLength(intarray);

    //方案一 通過GetIntArrayRegion指定buf指派範圍
//    jint *buf ;
//
//    env->GetIntArrayRegion(intarray, 0, length, buf);
//    for (jint i = 0; i < length; ++i) {
//        sum += buf[i];
//    }

    //方案二:通過GetIntArrayElements和ReleaseIntArrayElements,
    //傳回 Java 數組的一個拷貝(實作優良的VM,會傳回指向 Java 數組的一個直接的指針,并标記該記憶體區域,不允許被 GC)。
    jint *pInt = env->GetIntArrayElements(intarray, NULL);
    for (jint i = 0; i < length; ++i) {
        sum += pInt[i];
    }
    env->ReleaseIntArrayElements(intarray,pInt,0);
    return sum;
}
           

JNI Array 函數彙總表如下:

音視訊開發(十五):JNI與NDK的學習和使用

針對對象(非String和數組的對象)和對象數組

通過FindClass 擷取到jclass,這塊内容會涉及到引用的類型,比如GlobalRef 和 LocalRef,我們在下一篇會詳細學習實踐。而對Java對象的描述又涉及到 java的屬性和方法在JNI 簽名相關知識,我們來一起學習下。

4.3 java的屬性和方法在JNI 簽名

我們這一小節,學習Java熟悉和方法在JNI的簽名,實踐從本地代碼通路Java對象成員、調用 Java 方法。

簽名的作用:為了準确描述一件事物.

Java Vm 定義了類簽名,方法簽名;其中方法簽名是為了支援方法重載。

Java 語言支援兩種成員(field):(static)靜态成員和對象成員. 在 JNI 擷取和指派成員的方法是不同的. 同樣的,方法也是兩種方法: 靜态方法和對象方法。

我們先看下了解下java的屬性和方法在JNI 簽名對應關系,然後通過通過native修改java成員值以及調用java方法為例對其進行了解熟悉。

音視訊開發(十五):JNI與NDK的學習和使用

其中要特别注意的是:

  1. 類描述符開頭的'L'與結尾的';'必須要有
  2. 數組描述符,開頭的'['必須有.
  3. 方法描述符規則: "(各參數描述符)傳回值描述符",其中參數描述符間沒有任何分隔 符号

    描述符很重要,請爛熟于心. 寫 JNI,對于錯誤的簽名一定要特别敏感,音視訊開發知識點路線圖:https://docs.qq.com/doc/DQm1VTHBlQmdmTlN2,此時編譯器幫不 上忙,執行 make 前仔細檢查你的代碼。

下面我們開始通過修改native修改java 成員變量和和調用方法修改修改java變量的值。

通路對象成員分三步,
1. 通過 GetObjectClass 從 obj 對象得到 cls.
2. 通過 GetFieldID 得到對象成員 ID, 如下:
fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
3. 通過在對象上調用下述方法獲得成員的值:
jstr = (*env)->GetObjectField(env, obj, fid);
此外 JNI 還提供Get/SetIntField,Get/SetFloatField 通路不同類型成員。
           

先來看下通過JNI通路修改java屬性的例子

//Java 代碼 定義兩個成員變量:靜态變量和執行個體變量

public class MainActivity extends AppCompatActivity {

   
    private String value = "123";
    private static String value_static = "321";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText(stringFromJNI()+appendString("hengheng","hahah")+sumArray(intarry)
        +"\n"+"value="+value+" value_static="+value_static);
    }
}

//JNI層
void accessField(JNIEnv *pEnv, jobject pJobject);

extern "C" JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject obj) {
    ...

    accessField(env,obj);

    return tmp;

}

void accessField(JNIEnv *env, jobject obj) {
    
    //1. 擷取class
    jclass jclazz = env->GetObjectClass(obj);
    
    //2. 通過GetFieldID擷取fieldid
    // 簽名一定要熟悉,否則在運作時直接會導緻崩潰。比如String對應簽名時"Ljava/lang/String;"
    jfieldID fieldId = env->GetFieldID(jclazz, "value", "Ljava/lang/String;");


    jstring jst = static_cast<jstring>(env->GetObjectField(obj, fieldId));

    jst = env->NewStringUTF("456");

    //3. 通過SetObjectField,修改fieldId的值
    env->SetObjectField(obj, fieldId, jst);

    
    
    ///下面來修改靜态成員

    //1. 通過GetStaticFieldID擷取靜态成員fieledid
    jfieldID fielId2 = env->GetStaticFieldID(jclazz, "value_static", "Ljava/lang/String;");

    jstring jst2 = env->NewStringUTF("789");
    //2. 通過SetStaticObjectField給靜态成員變量指派
    env->SetStaticObjectField(jclazz,fielId2,jst2);
    
}
           

接着我們來看下 JNI調用Java的方法

JNI通路Java方法的步驟:
1.通過 GetMethodID 在給定類中查詢方法. 查詢基于方法名稱和簽名
2.本地方法調用 Call<Return Value Type>Method

方法簽名由各參數類型簽名和傳回值簽名構成. 參數簽名在前,并用小括号括
起.
           
public class MainActivity extends AppCompatActivity {

    ...

    //定義執行個體方法
    private void setValue(String value) {
        this.value = value;
    }

    //定義靜态方法
    private static void setValue_static(String value_static) {
        MainActivity.value_static = value_static;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText(stringFromJNI()+appendString("hengheng","hahah")+sumArray(intarry)
        +"\n"+"value="+value+" value_static="+value_static);
    }
}


//JNI實作

void accessMethod(JNIEnv *env, jobject obj) {
    jclass clazz = env->GetObjectClass(obj);

    jmethodID methodId = env->GetMethodID(clazz, "setValue", "(Ljava/lang/String;)V");
    if (methodId == NULL) {
        return;
    }
    //這裡一定要注意 不能直接env->CallVoidMethod(obj,methodId,"set by native method");
    //JNI中對象類型的使用一定用通過env方法來操作,比如生成string:env->NewStringUTF
    jstring jst = env->NewStringUTF("set by native method");
    env->CallVoidMethod(obj,methodId,jst);
    
    
    //調用靜态方法
    jmethodID methodId1= env->GetStaticMethodID(clazz, "setValue_static", "(Ljava/lang/String;)V");
    if(methodId1== NULL){
        return;
    }
    jstring jst1 = env->NewStringUTF("\n set by native method staic");

    env->CallStaticVoidMethod(clazz,methodId1,jst1);

}
           

 這裡遇到了一個問題,折騰了好一陣。問題是:

使用 env->CallVoidMethod(obj,methodId, "set by native method");運作時報錯

JNI DETECTED ERROR IN APPLICATION: use of deleted global reference 0x71749eae2e

檢查簽名沒有錯,通過java -p class 也确認了簽名的正确性,但是為什麼報錯呐?

[How can I call Java Methods containing String parameter(s) using JNI?]: http://www.jguru.com/faq/view.jsp?EID=226786

看到了正确的用法,突然意識到在JNI中java的String不是基本資料類型,資料的生成要通過env對應的方法擷取。

改為

jstring jst = env->NewStringUTF("set by native method");
env->CallVoidMethod(obj,methodId,jst);
           

另外還可以通過 JNI 調用 Java 類的構造方法和父類的方法

4.4 性能優化

執行一個 Java/native 調用要比 Java/Java 調用慢 2-3 倍. 也可能有一些 VM 實作,Java/native 調用性能與 Java/Java 相當。(此種虛拟機,Java/native 使用 Java/Java相同的調用約定)。

native/Java 調用效率可能與 Java/Java 有 10 倍的差距,因為 VM 一般不會做 Callback 的優化。

通過 FindClass 、GetFieldID、GetMethodID 去找到對應的資訊是很耗時的,如果方法被頻繁調用,那麼肯定不能每次都去查找對應的資訊,有必要将它們緩存起來,在下一次調用時,直接使用緩存内容就好了

可以在項目中加一套 Hash 表, 封裝 FindClass,GetMethodID,GetFieldID等函數,查詢的所有操作,都對 Hash 表操作,如首次 FindClass 一個類,這時可以把一個類的所有成員緩存到 Hash 表中,用名字+簽名做鍵值。

引入了這個優化,項目的執行效率有 100 倍的提高;

  1. 用一個 Hash 表,還是每個類一個 Hash 表
  2. 首次 FindClass 類時,一次緩存所有的成員,還是用時緩存

    最終做的選擇是:為了降低沖突,每個類一個 Hash 表,并且一次緩存一個類的所有成員。

五、Java和Native的互相調用

上面兩個小節中在對Java 語言中的資料類型是如何映射到 c/cpp本地語言中的

以及 java的屬性和方法在JNI 簽名的學習實踐已經充分的展示了互相調用。如果還有不清晰,請回看上一小節。

這裡我們再來回顧下,這篇的目标 以及疑惑

JNI方法的參數 JNIEnv 和 jobject代碼什麼意思

Java中需要的是String類型,為什麼JNI傳回的是一個jstring類型?

—》這個兩個問題,相信通過上面的學習實踐,已經有很好的了解

我們再來其他幾個問題

stringFromJNI生成的關聯的Native方法名稱為什麼是Java_com_av_mediajourney_MainActivity_stringFromJNI?可以是其他的嗎?

JNI的方法名稱是根據java的全包名+類名,并且把”.”替換為”_”,為規則生成的。音視訊開發知識點路線圖:https://docs.qq.com/doc/DQm1VTHBlQmdmTlN2,這是靜态注冊的方式,當然也有動态注冊的方式,這個我們下一篇再來詳細學習實踐。

extern "C" 是什麼意思?

extern “C”的作用是避免編譯器按照CPP的方式編譯C函數

C語言不支援函數的重載,編譯之後函數名稱不變

CPP支援函數的重載,編譯之後函數名稱會發生變化。調用的時候導緻找不到JNI的實作

JNIEXPORT和JNICALL又是什麼意思?

在jni.h中可以看到這兩個宏的定義
//JNIEXPORT表示 該函數是否可以導出
#define JNIEXPORT  __attribute__ ((visibility ("default")))
//調用規範
#define JNICALL
           

這篇就到這裡了。感謝你的閱讀

通過對JNI和NDK的學習實踐,

  1. 了解了JNI和NDK是什麼,以及兩者之間的關系;
  2. Android如何配置進行NDK的開發
  3. JNI基本知識介紹(JNIEnv、資料類型對應關系、屬性和方法簽名等)
  4. 實作Android中Java和Native的互相調用
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓

繼續閱讀