Android:JNI與NDK(一)

友情提示:歡迎關注本人公衆號,那裏有更好的閱讀體驗以及第一時間獲取最新文章

 

本篇目錄


如下舉例代碼均來自:NDK示例代碼
java

1、前言

安卓開發中不少場景須要用到NDK來開發,好比,音視頻的渲染,圖像的底層繪製,祕籍計算應用,複用C/C++庫等等,安卓絕大部分核心代碼都是在Native層來完成,也就是用C/C++來完成,有的時候咱們看系統源碼的時候追着追着就發現最終調用一個native聲明的方法,接下來就須要深刻native層來查看具體邏輯了,那java代碼是怎麼調用native層代碼的呢?或者說java是怎麼調用C/C++代碼的呢?這裏就用到JNI/NDK方面技術了,本系列不會細講C/C++語言知識,語言方面須要你本身私下學習,若是你想深刻NDK層學習,那麼請務必先學習一下C/C++語言知識,起碼能看得懂啊,學習的時候能夠嘗試用C/C++來刷LeetCode,防止不用慢慢就忘記了,好了,接下來咱們進入本篇正題。linux

2、什麼是JNI/NDK

JNI

JNI是java的特性,與安卓無關,用來加強java與本地代碼交互的能力,JNI是Java的一個框架,定義了一系列方法能夠用於Java與C/C++互相調用。android

NDK

NDK是安卓平臺的開發工具包,是安卓的特性,與java無關,用來快速開發生成C、 C++的動態庫,經過 NDK咱們能夠在 Android中將C/C++代碼編譯到原生庫中,而後使用 IDE 集成構建系統 Gradle 將您的庫封裝入 APK。c++

JNI是Java特性,在window平臺能夠用java的JNI特性來完成java與C/C++互相調用,linux平臺也能夠,NDK是安卓平臺的開發工具包,在安卓開發的時候咱們能夠經過Java的JNI特性來完成java與C/C++互相調用,可是C/C++代碼怎麼編譯到原生庫中呢?這時就用安卓平臺提供的NDK開發工具了。git

接下來咱們就來看一下具體實現Java與C/C++互調。github

3、Java與C/C++互調

AS配置NDK環境在3.0以上已經十分簡單了,環境的配置請自行查閱搭建,這裏咱們直接講解Java與C/C++互調知識。數組

JNI數據類型

JNI數據類型與Java數據類型對應以下:app

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方法:ide

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屬性, 該屬性是說, 啓用這個屬性:

  1. 當visibility=hidden時

動態庫中的函數默認是被隱藏的即 hidden. 除非顯示聲明爲__attribute__((visibility("default"))).

  1. 當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的簽名。

4、靜態註冊與動態註冊以及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的開發。

5、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()方法釋放線程資源。

6、總結

本篇算是NDK開發的入門篇,介紹了一些基礎的操做,必定要記住,若是想深刻NDK層先把C/C++語言基礎打好,不然上面代碼看起來很蒙圈,後續文章讀起來也很難受。

相關文章
相關標籤/搜索