目錄
- 什麼是JNI、NDK?
- Java和Native互動流程
- 通過AS建立Native CPP簡單的項目
- JNI基礎知識介紹
- 實作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
- 在Java類中通過native關鍵字聲明Native方法
- javac指令編譯Java類得到class檔案
- 通過javah指令(javah -jni class名稱)導出JNI的頭檔案(.h檔案)
- 實作native方法
- 編譯生成動态庫(.so檔案)
- 實作Java和C、CPP的互相調用
NDK
- 配置NDK環境、建立Natvie CPP項目)
- 在Java類中通過native關鍵字聲明Native方法
- 自動生成native方法,實作native方法
- 通過ndk-build或者cmake編譯産生動态庫
- 實作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是編譯工具
配置好環境後,我們就可以開始建立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方法可能會有以下疑問。
- stringFromJNI生成的關聯的Native方法名稱為什麼是Java_com_av_mediajourney_MainActivity_stringFromJNI?可以是其他的嗎?
- JNI方法的參數 JNIEnv* 和 jobject代碼什麼意思?*
- Java需要的是String類型,為什麼JNI傳回的是一個jstring類型?
- extern "C" 是什麼意思?
- JNIEXPORT和JNICALL又是什麼意思?
這涉及到JNI的基本知識,我們通過對JNI基本知識的學習來解決上面的疑惑。
四、JNI基本知識
本小節分如下内容
- JNIEnv和jobject jclass
- Java 語言中的資料類型是如何映射到 c/cpp本地語言中的
- java的屬性和方法在JNI 簽名
4.1 JNIEnv 和 jobject 、 jclass
JNIEnv:是指線程上下文環境,每個線程有且隻有一個JNIEnv執行個體
JNIEnv 結構包括 JNI 函數表
第二個參數的意義取決于該方法是靜态還是執行個體方法(static or an instance method)。音視訊開發知識點路線圖:https://docs.qq.com/doc/DQm1VTHBlQmdmTlN2,當本地方法作為一個執行個體方法時,第二個參數相當于對象本身,即 this. 當本地方法作為一個靜态方法時,指向所在類.
4.2 Java 語言中的資料類型是如何映射到 c/cpp本地語言中的
在 Java 中有兩類資料類型:基本資料類型,如,boolean,int, float, char;另一種為引用資料類型,如,類,執行個體,數組。
Java和JNI的基本資料類型映射關系如下
相比基本類型,對象類型的傳遞要複雜很多。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函數彙總表如下:
針對數組
通過求和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 函數彙總表如下:
針對對象(非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方法為例對其進行了解熟悉。
其中要特别注意的是:
- 類描述符開頭的'L'與結尾的';'必須要有
- 數組描述符,開頭的'['必須有.
-
方法描述符規則: "(各參數描述符)傳回值描述符",其中參數描述符間沒有任何分隔 符号
描述符很重要,請爛熟于心. 寫 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 倍的提高;
- 用一個 Hash 表,還是每個類一個 Hash 表
首次 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的學習實踐,
- 了解了JNI和NDK是什麼,以及兩者之間的關系;
- Android如何配置進行NDK的開發
- JNI基本知識介紹(JNIEnv、資料類型對應關系、屬性和方法簽名等)
- 實作Android中Java和Native的互相調用
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓