接上一篇,搭建好基於Android Studio的環境之後,編寫native程式碼相對來說也比較簡單了。在Android上編寫Native程式碼和在Linux編寫C/C++程式碼還是有區別,Native程式碼一般需要與JVM互動資料,需要遵循一定的規範,本文來介紹一下基本的JNI程式碼寫法。
我們還是從例項出發,配置好Android Studio工程之後,我們需要建立jni目錄和在jni目下建立c/c++檔案和相應的標頭檔案,建立方式見下圖。
在例項工程中我們建立了NdkSample.cpp 和 NdkSample.h,原始碼見下面:
#include "NdkSample.h" JNIEXPORT jstring JNICALL Java_com_zyp_ndktest_MainActivity_sayHello (JNIEnv *env, jclass cls, jstring j_str) { const char *c_str = nullptr; char buff[128] = {0}; jboolean isCopy; c_str = env->GetStringUTFChars(j_str, &isCopy); printf("isCopy:%d\n",isCopy); if(c_str == NULL) { return NULL; } printf("C_str: %s \n", c_str); sprintf(buff, "hello %s", c_str); env->ReleaseStringUTFChars(j_str, c_str); return env->NewStringUTF(buff); }
#ifndef NDKTEST_NDKSAMPLE_H #define NDKTEST_NDKSAMPLE_H #include "jni.h" #include <stdio.h> #include <string.h> extern "C" { JNIEXPORT jstring JNICALL Java_com_zyp_ndktest_MainActivity_sayHello(JNIEnv *env, jclass type, jstring filename); } #endif //NDKTEST_NDKSAMPLE_H
現在來簡單介紹一下,首先是NdkSample.h檔案,剛剛建立的時候只有相應的預處理命令,我們在標頭檔案預處理命令之間加上 jni.h ,stdio.h ,string.h 後兩個非必要。將我們要在java層呼叫的介面宣告出來,放在extern "c"{} 中(告訴編譯器按照C標準進行編譯)。第一次接觸jni的同學看到那麼複雜的函式命名和奇怪的JNIEXPORT ,JNICALL,JNIEnv之類的估計有點不習慣,本文就不詳細介紹它們的意思,其實你跟蹤原始碼它們就是幾個巨集(JNIEnv是一個結構體儲存當前環境的上下文),其它的jstring,jclass之類的很好理解就是在Native環境中對JVM中java對應結構的一種表示方式。
函式命令方式是包名加activity名加函式名,表如我們在java層中的包名是java.com.zyp.ndktest,在MainActivity中呼叫sayHello函式,則jni層函式命名就要寫成上面的方式。
接下來看NdkSample.cpp檔案中函式的定義。ni層的函式還需要多兩個引數,一個是JNIEnv * ,一個是jclass。我們在java層呼叫的時候就只用傳遞前兩個引數之外的引數。例子中我們想從java層傳遞一個String型別的引數到jni層,jni層從JVM中取資料的時候取到的卻是jstring型別,在jni層我們不能直接使用需要轉換。這裡我們通過env->GetStringUTFChars(j_str, &isCopy)函式來完成,將j_str所在的地址轉換並賦值給const char *型別的指標,之後我們就可以通過該指標訪問那塊記憶體了。注意這裡是const char * 表示該指標指向的記憶體區域的內容是不可以改變的,java中的String 也是自帶final屬性的。GetStringUTFChars()實際上是將JVM內部的Unicode轉化成為了C/C++認識的UTF-8的格式的字串,注意這個函式內部發生了記憶體分配,相當於是拷貝了一份Unicode然後進行轉化,所以後面需要ReleaseStringUTFChars()來釋放記憶體。
最後該函式返回一個新的構建好的jstring型別給java層,為了將C/C++層的UTF-8字串轉換為JVM中的Unicode字串,需要呼叫另外一個函式NewStringUTF()來完成轉換。
我們注意到JVM中的內容jni中不能直接操作需要進行轉換,jni中的內同也要進行轉換,因此也有大量的相關jni介面存在,後面文章中會挑選一些來講解。
此外,函式中JNIEnv *env這個引數需要說一下,在C和C++中使用方式是不一樣的,不要搞混了。在C中,看到JNIEnv 我們實質是取得了JNINativeInterface* (JNIEnv指標的指標),我們得使用**env獲取結構體,從而才能使用結構體裡面的方法。在C++中,看到JNIEnv我們實質是取得了JNIEnv*(JNIEnv結構體的指標),我們可以直接使用env->使用結構體裡面的方法。注意我們呼叫GetStringUTFChars()的方式,但是注意和Java_com_zyp_ndktest_MainActivity_sayHello()進行區別。
現在來看看我們在java層中如何呼叫jni層的介面,看下面程式碼:
package com.zyp.ndktest; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String ret = sayHello("zhuzhu"); Log.i("JNI_INFO", ret); } static { System.loadLibrary("NdkSample"); } public native static String sayHello(String str); }
我們首先要通過System.loadLibrary()載入jni程式碼編譯後生成的.so,但是這個庫的名字怎麼來的呢,注意回過頭去看上一篇中gradle ndk{}中的內容,我們是在那裡進行的命名的;然後還要宣告native static 型別的該函式。然後直接呼叫就好了。執行結果見下圖。
希望通過這篇文章能夠讓大家入門JNI開發^_^。