友情提示:歡迎關注本人公眾號,那里有更好的閱讀體驗以及第一時間獲取最新文章
本篇目錄
以下舉例代碼均來自:NDK示例代碼
一、前言
安卓開發中很多場景需要用到NDK來開發,比如,音視頻的渲染,圖像的底層繪制,秘籍計算應用,復用C/C++庫等等,安卓絕大部分核心代碼都是在Native層來完成,也就是用C/C++來完成,有的時候我們看系統源碼的時候追着追着就發現最終調用一個native聲明的方法,接下來就需要深入native層來查看具體邏輯了,那java代碼是怎么調用native層代碼的呢?或者說java是怎么調用C/C++代碼的呢?這里就用到JNI/NDK方面技術了,本系列不會細講C/C++語言知識,語言方面需要你自己私下學習,如果你想深入NDK層學習,那么請務必先學習一下C/C++語言知識,起碼能看得懂啊,學習的時候可以嘗試用C/C++來刷LeetCode,防止不用慢慢就忘記了,好了,接下來我們進入本篇正題。
二、什么是JNI/NDK
JNI
JNI是java的特性,與安卓無關,用來增強java與本地代碼交互的能力,JNI是Java的一個框架,定義了一系列方法可以用於Java與C/C++互相調用。
NDK
NDK是安卓平台的開發工具包,是安卓的特性,與java無關,用來快速開發生成C、 C++的動態庫,通過 NDK我們可以在 Android中將C/C++代碼編譯到原生庫中,然后使用 IDE 集成構建系統 Gradle 將您的庫封裝入 APK。
JNI是Java特性,在window平台可以用java的JNI特性來完成java與C/C++互相調用,linux平台也可以,NDK是安卓平台的開發工具包,在安卓開發的時候我們可以通過Java的JNI特性來完成java與C/C++互相調用,但是C/C++代碼怎么編譯到原生庫中呢?這時就用安卓平台提供的NDK開發工具了。
接下來我們就來看一下具體實現Java與C/C++互調。
三、Java與C/C++互調
AS配置NDK環境在3.0以上已經十分簡單了,環境的配置請自行查閱搭建,這里我們直接講解Java與C/C++互調知識。
JNI數據類型
JNI數據類型與Java數據類型對應如下:
Java類型 | 本地類型 |
---|---|
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
Object | jobject |
Class | jclass |
String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
這些對應關系什么意思呢?接下來通過具體實例了解一下:
創建新項目,我們在MainActivity中聲明如下native方法:
1 native int arrayTest(int i,int[] a1, String[] a2);
意思是這個方法需要native層來實現,java調用的時候會傳遞三個參數,分別是:int ,int[] , String[] 類型的,接下來我們需要在native層來實現這個方法,AS中通過快捷鍵"alt+/"會自動幫助我們在native層來實現方法的聲明:
1 JNIEXPORT jint JNICALL Java_com_wanglei55_ndk_MainActivity_arrayTest(JNIEnv *env,
2 jobject instance,jint i,jintArray a1_,jobjectArray a2)
方法聲明生成規則為:Java_包名_類名_方法名
java中聲明的arrayTest方法參數類型分別為int,int[],String[]類型,在JNI中生成的方法聲明分別對應jint ,jintArray ,jobjectArray ,這里就用到了上面的數據類型對應表,至於其余參數類型依照上表對應即可。
我們觀察JNI中方法聲明還發現生成的方法對了一些額外信息:JNIEXPORT ,JNICALL,參數中多了JNIEnv *env, jobject instance這些又都是什么鬼?我們一一解釋
JNIEXPORT
在 Windows 中,定義為__declspec(dllexport)
。因為Windows編譯 dll 動態庫規定,如果動態庫中的函數要被外部調用,需要在函數聲明中添加此標識,表示將該函數導出在外部可以調用。
在 Linux/Unix/Mac os/Android 這種類Unix系統中,定義為__attribute__ ((visibility ("default")))
GCC 有個visibility屬性, 該屬性是說, 啟用這個屬性:
-
當visibility=hidden時
動態庫中的函數默認是被隱藏的即 hidden. 除非顯示聲明為__attribute__((visibility("default")))
.
-
當visibility=default時
動態庫中的函數默認是可見的.除非顯示聲明為__attribute__((visibility("hidden")))
.
JNIEXPORT 主要用於window平台,在安卓平台可不加,去掉即可。
JNICALL:
在類Unix中無定義,在Windows中定義為:_stdcall
,一種函數調用約定
在安卓平台 定義如下:
#define JNICALL 什么也沒定義
所以,同JNIEXPORT 一樣在安卓平台JNICALL可不加,去掉即可。
jobject instance:
在AS中自動為我們生成的JNI方法聲明都會帶一個這樣的參數,這個instance就代表Java中native方法聲明所在的類,比如上面arrayTest方法聲明在MainActivity中,這里的instance就表示MainActivity實例。
JNIEnv *env:
JNIEnv 指針可是JNI中非常非常重要的一個概念,代表了JNI的環境,JNI層實現的方法都是通過這個指針來調用,通過JNIEnv 指針我們可以調用JNI層的方法訪問Java虛擬機,進而操作Java對象。
JNIEnv 指針只在創建它的線程有效,不能跨線程傳遞,對於這句話的理解我們會在后面涉及線程的時候會再次提到,這里不懂可以看完全文回來再看一下。
我們看下JNIEnv 是怎么定義的:
jni.h中對JNIEnv定義如下:
1 #if defined(__cplusplus) //c++環境 2 typedef _JNIEnv JNIEnv;//c++環境中JNIEnv為_JNIEnv 3 typedef _JavaVM JavaVM; 4 #else 5 typedef const struct JNINativeInterface* JNIEnv;//c環境JNIEnv為const struct JNINativeInterface* 6 typedef const struct JNIInvokeInterface* JavaVM; 7 #endif
C++中JNIEnv為_JNIEnv 而 C環境JNIEnv為const struct JNINativeInterface*
我們先看_JNIEnv,定義如下:
1 struct _JNIEnv { 2 3 const struct JNINativeInterface* functions; 4 5 #if defined(__cplusplus) 6 7 jint GetVersion() 8 { return functions->GetVersion(this); } 9 10 jclass DefineClass(const char *name, jobject loader, const jbyte* buf, 11 jsize bufLen) 12 { return functions->DefineClass(this, name, loader, buf, bufLen); } 13 14 jclass FindClass(const char* name) 15 { return functions->FindClass(this, name); } 16 。。。。。 17 }
_JNIEnv 是對 const struct JNINativeInterface類型的包裝,間接調用了const struct JNINativeInterface 上定義的方法。
我們繼續看JNINativeInterface定義:
1 struct JNINativeInterface { 2 。。。 3 jint (*GetVersion)(JNIEnv *); 4 jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, 5 jsize); 6 jclass (*FindClass)(JNIEnv*, const char*); 7 jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); 8 jfieldID (*FromReflectedField)(JNIEnv*, jobject); 9 /* spec doesn't show jboolean parameter */ 10 jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean); 11 。。。 12 }
這里才是接口真正定義的地方,具體的實現在Java虛擬機中。
通過以上分析,我們得出以下結論:
-
C++中JNIEnv *env相當於 struct _JNIEnv *env 調用方法只需如下方式即可間接調用JNINativeInterface 中方法:
1 env-> FindClass(JNIEnv*, const char*)
-
C中JNIEnv *env相當於 JNINativeInterface **env,二級指針,調用方法需要先解引用在調用如下:
1 (*env)-> FindClass(JNIEnv*, const char*)
明白了以上概念后我們可以繼續在native層來實現
1 JNIEXPORT jint JNICALL 2 Java_com_wanglei55_ndk_MainActivity_arrayTest(JNIEnv *env,jobject instance,jint i,jintArray a1_,jobjectArray a2)
方法了。
使用Java層傳遞過來的數據
Java層傳遞過來的數據可能為基本數據類型,數組,對象等,不同數據類型我們要想使用需要不同的處理方式,具體如下。
基本類型數據
Java層傳遞過來的基本數據類型無需其余操作,直接使用即可。
數組類型數據
數組分為基本數據類型的數組與對象數據類型的數組,比如,int[]與String[],在Native我們怎么獲取數組中的數據呢?如下:
1 JNIEXPORT jint JNICALL 2 Java_com_wanglei55_ndk_MainActivity_arrayTest(JNIEnv *env, 3 jobject instance,jint i,jintArray a1_,jobjectArray a2) { 4 LOGE("i的值為:%d", i); 5 // 第二個參數: 6 // true:拷貝一個新數組 7 // false: 就是使用的java的數組 (地址) 8 jint *a1 = env->GetIntArrayElements(a1_, 0);//返回指針,指向數組地址 9 jsize len = env ->GetArrayLength(a1_);//獲取數組長度 10 for (int i = 0; i < len; ++i) { 11 LOGE("int數組的值為:%d", *(a1+i)); 12 //改變java中數組的值,如果下面參數3 mode設置為2則改變不了 13 *(a1+i) = 666; 14 } 15 // 參數3:mode 16 // 0: 刷新java數組 並 釋放c/c++數組 17 // 1 = JNI_COMMIT:只刷新java數組 18 // 2 = JNI_ABORT:只釋放 19 env->ReleaseIntArrayElements(a1_, a1, 0); 20 // 21 jsize slen = env->GetArrayLength(a2);//獲取數組長度 22 for (int i = 0; i < slen; ++i) { 23 jstring str = static_cast<jstring>(env->GetObjectArrayElement(a2, i));//獲取數組中的數據 24 const char* s = env->GetStringUTFChars(str,0); 25 LOGE("jni獲取java字符串數組:%s", s); 26 env->ReleaseStringUTFChars(str, s); 27 } 28 return 3; 29 }
上面展示了native層獲取java傳遞過來的數組數據,這里只是遍歷了一下,可以看到核心方法都是通過JNIEnv 指針來調用方法操作的,所以JNIEnv 是十分重要的。
對象類型數據
Java傳遞過來的對象怎么處理呢?這里需要用到反射了,同樣也是通過JNIEnv 指針來調用相應方法的,我們在MainActivity添加如下方法:
1 native void objectTest(Student s, String str);
Student 類如下:
1 public class Student { 2 3 private int num = 100; 4 5 public int getNum() { 6 return num; 7 } 8 9 public void setNum(int num) { 10 this.num = num; 11 } 12 13 public static void printMsg(Card card){//調用方法需要傳遞Card類 14 Log.e("JNI","printMsg Card: "+card.id); 15 } 16 17 public static void printMsg(String str){ 18 Log.e("JNI","printMsg: "+str); 19 } 20 }
Card類:
1 public class Card { 2 int id; 3 4 public Card(int id) { 5 this.id = id; 6 } 7 }
都很簡單,這里就是演示一下。
接下來我們看下native層怎么獲取傳遞過來的對象數據以及調用其方法,這里我們直接看代碼,注釋給了詳細的說明:
1 extern "C" 2 JNIEXPORT void JNICALL 3 Java_com_wanglei55_ndk_MainActivity_objectTest(JNIEnv *env, jobject instance, jobject bean, 4 jstring str_) { 5 // 6 const char *str = env->GetStringUTFChars(str_, 0); 7 LOGE("objectTest: %s",str); 8 env->ReleaseStringUTFChars(str_, str); 9 //bean就是java層傳遞過來的Student對象 10 //反射方式調用bean中的set/get方法 11 jclass beanClass = env->GetObjectClass(bean);//獲取class 12 //修改屬性值 13 //jfieldID fieldID = env->GetFieldID(beanClass,"num","I"); 14 //env->SetIntField(bean,fieldID,444); 15 16 //調用set方法設置 17 jmethodID setMethodID = env->GetMethodID(beanClass,"setNum","(I)V");//獲取方法信息 18 env->CallVoidMethod(bean,setMethodID,999);//調用bean中的setMethodID對應的方法 19 //調用get方法獲取 20 jmethodID getMethodID = env->GetMethodID(beanClass,"getNum","()I");//獲取方法信息 21 jint result = env->CallIntMethod(bean,getMethodID); 22 LOGE("調用Student中getNum返回值: %d",result); 23 24 //調用靜態方法:public static void printMsg(String str) 25 jmethodID staticMID = env->GetStaticMethodID(beanClass,"printMsg","(Ljava/lang/String;)V"); 26 jstring jstring1 = env->NewStringUTF("JNI中的String"); 27 env->CallStaticVoidMethod(beanClass,staticMID,jstring1); 28 env->DeleteLocalRef(jstring1);//釋放 29 30 //調用靜態方法:public static void printMsg(Card card) 31 jmethodID staticMID2 = env->GetStaticMethodID(beanClass,"printMsg","(Lcom/wanglei55/ndk/Card;)V"); 32 //創建參數Card 33 jclass cardclz = env->FindClass("com/wanglei55/ndk/Card");//通過完整類名獲取class 34 jmethodID constructorID = env->GetMethodID(cardclz,"<init>","(I)V");//<init>表示獲取構造方法 35 jobject cardObj = env->NewObject(cardclz,constructorID,333);//反射創建Card對象 36 env->CallStaticVoidMethod(beanClass,staticMID2,cardObj); 37 env->DeleteLocalRef(cardObj); 38 }
上面已經給了詳細注釋,不再說明,這里需要額外說一下方法的簽名。
方法簽名
調用GetMethodID與GetStaticMethodID的時候我們需要傳遞方法的簽名信息,怎么配置呢?如下有個對應表:
Java類型 | 簽名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用類型 | L + 全限定名 + ; |
數組 | [+類型簽名 |
如果有內部類 則用$來分隔 如:Landroid/os/FileUtils$FileStatus;
什么意思呢?
比如以Student類中getNum()方法為例,其定義如下:
1 public int getNum()
方法調用不用傳遞參數,返回值為int類型,int對應簽名為I,大寫的啊,所以方法簽名為"()I",()里面填寫參數對應的簽名,()右面緊跟方法返回值簽名。
再來個復雜的,比如如下方法:
1 String getInfo(long[], List list);
簽名是什么呢?其簽名為:
1 "([JLjava/util/List)Ljava/lang/String;"
其中 "[J" 代表long[]的簽名,"Ljava/util/List" 代表List list的簽名,"Ljava/lang/String;" 代表返回值String的簽名。
四、靜態注冊與動態注冊以及JNI_OnLoad方法
靜態注冊
像上面我們在java層定義native方法:
1 native void objectTest(Student s, String str);
然后在JNI層定義對應方法:
1 JNIEXPORT void JNICALL 2 Java_com_wanglei55_ndk_MainActivity_objectTest(JNIEnv *env, jobject instance, jobject bean,jstring str_) { 3 。。。 4 }
當我們在Java中調用objectTest(Student s, String str)方法時,就會從JNI層尋找Java_com_wanglei55_ndk_MainActivity_objectTest(JNIEnv *env, jobject instance, jobject bean,jstring str_) 方法,並為二者建立聯系。
靜態注冊就是根據方法名,將Java層native方法和JNI層對應方法建立關聯,這種方式就是靜態注冊,靜態注冊有如下缺點:
-
JNI層方法名很長
-
第一次調用native方法會比較耗時,需要查找對應方法建立聯系(通過指針記錄方法)
有沒有一種方式在加載的時候就建立起二者的聯系呢?這樣第一次調用native方法的時候就不需要查找了,這種方式就是動態注冊。
動態注冊
動態注冊可以在加載的時候就建立起java層native方法與JNI層方法的聯系,那具體怎么建立聯系呢?加載的時候是指什么時候?
我們在調用動態庫so中方法的時候都會先加載對應so庫,比如:
1 static { 2 System.loadLibrary("native-lib"); 3 }
在加載native-lib動態庫的時候JVM會檢查對應C/C++文件中是否有int JNI_OnLoad(JavaVM *vm, void *reserved)方法,有的話則會調用這個方法,在這個方法里面我們可以做一些初始化的操作,進而可以動態注冊一些方法。
接下來我們具體操作一下看看怎么動態注冊:
首先java層同樣定義native方法,如下:
1 native void dynamicJavaTest(); 2 native int dynamicJavaTest2(int i);
接下來在JNI層定義對應方法:
1 void dynamicTest(){ 2 LOGE("JNI dynamicTest"); 3 } 4 5 jint dynamicTest2(JNIEnv *env, jobject instance,jint i){ 6 LOGE("JNI dynamicTest2:%d",i); 7 return 9999; 8 }
這里我並沒有把方法名設置為一樣,方法名你可以隨便起,如果想接收JNIEnv *env, jobject instance參數可以在方法上加上,Jvm調用的時候會傳遞這兩個參數給JNI層方法,不想接收也可以去掉。
java層方法與JNI層怎么建立起關聯呢?接下來我們還需要定義JNINativeMethod類型的數組,將兩者對應起來,JNINativeMethod定義在jni.h中定義如下:
1 typedef struct { 2 const char* name;//java層的方法名 3 const char* signature;//java層方法的簽名 4 void* fnPtr;//JNI層對應方法的指針 5 } JNINativeMethod;
這里我們將java層dynamicJavaTest方法與JNI層dynamicTest對應
java層dynamicJavaTest2方法與JNI層dynamicTest2對應
所以數組定義如下:
1 static const JNINativeMethod methods[] = { 2 {"dynamicJavaTest","()V",(void*)dynamicTest}, 3 {"dynamicJavaTest2","(I)I",(int*)dynamicTest2}, 4 };
接下來就可以在JNI_OnLoad方法中動態注冊了:
1 static const char *mClassName = "com/wanglei55/ndk/MainActivity"; 2 3 JavaVM *_vm;//記錄JavaVM 4 5 int JNI_OnLoad(JavaVM *vm, void *reserved){ 6 // 7 LOGE("JNI_Onload"); 8 // 9 _vm = vm; 10 // 獲得JNIEnv 11 JNIEnv *env = 0; 12 // 小於0 失敗 ,等於0 成功 13 int r = vm->GetEnv((void**)&env,JNI_VERSION_1_4); 14 if (r != JNI_OK){ 15 return -1; 16 } 17 //獲得 class對象 18 jclass jcls = env->FindClass(mClassName); 19 //動態注冊方法 20 env->RegisterNatives(jcls,methods, sizeof(methods)/ sizeof(JNINativeMethod)); 21 return JNI_VERSION_1_4;// 返回native 組件使用的 JNI 版本 22 }
核心就是調用RegisterNatives方法來完成動態注冊的邏輯,到此動態注冊就完成了,此外動態注冊不用定義那么長的方法。
在安卓系統源碼中JNI層大量使用了動態注冊方法而不是靜態注冊,靜態注冊多用於平常NDK的開發。
五、native線程調用Java
native調用java需要用到JNIEnv指針,而JNIEnv是由Jvm傳入與線程相關的變量,如果我們在native中開啟一個線程完成工作后回調java層方法怎么辦呢?可以通過JavaVM的AttachCurrentThread方法來獲取到當前線程中JNIEnv指針。
接下來我們看一下怎么操作。
java層定義native方法與回調的方法:
1 public void callBack(){ 2 if (Looper.myLooper() == Looper.getMainLooper()){ 3 Toast.makeText(this,"MainLooper",Toast.LENGTH_SHORT).show(); 4 }else{ 5 runOnUiThread(new Runnable() { 6 @Override 7 public void run() { 8 Toast.makeText(MainActivity.this,"runOnUiThread",Toast.LENGTH_SHORT).show(); 9 } 10 }); 11 } 12 } 13 14 native void testThread();
JNI層采用靜態注冊的方式注冊對應方法:
1 jobject _instance; 2 3 void* threadTask(void* args){ 4 // native線程 附加 到 Java 虛擬機 5 JNIEnv *env;//JNIEnv *是與線程有關的 6 //調用JavaVM 的AttachCurrentThread方法來獲取與線程有關的JNIEnv 7 jint i = _vm->AttachCurrentThread(&env,0);//JNI_OnLoad會傳遞過來JavaVM *vm參數 8 if (i != JNI_OK){ 9 return nullptr; 10 } 11 //回調 12 //獲得MainActivity的class對象 13 jclass cls = env->GetObjectClass(_instance); 14 jmethodID updateUI = env->GetMethodID(cls,"callBack","()V"); 15 env->CallVoidMethod(_instance,updateUI); 16 //釋放內存 17 env->DeleteGlobalRef(_instance); 18 //退出線程,釋放線程資源 19 _vm->DetachCurrentThread(); 20 return 0; 21} 22 23 extern "C" 24 JNIEXPORT void JNICALL 25 Java_com_wanglei55_ndk_MainActivity_testThread(JNIEnv *env, jobject instance) { 26 27 pthread_t pid; 28 //啟動線程 29 _instance = env->NewGlobalRef(instance); 30 pthread_create(&pid,0,threadTask,0);//記得引入頭文件 #include <pthread.h> 31}
native線程中使用JNIEnv一定要記得獲取當前線程的JNIEnv,因為不同線程的JNIEnv是不同的,同時使用完記得調用DetachCurrentThread()方法釋放線程資源。
六、總結
本篇算是NDK開發的入門篇,介紹了一些基礎的操作,一定要記住,如果想深入NDK層先把C/C++語言基礎打好,否則上面代碼看起來很蒙圈,后續文章讀起來也很難受。