由於工作上的需求需要使用java和c++互調實現功能,所以要對jni進行深入研究,故此入坑。已經很久沒有寫介面佈局這方面的了,對於android開發來講,更加偏向於前端,不知道是好是壞...
JNI是什麼
JNI全程Java Native Interface,意為Java本地呼叫,它允許Java程式碼和其他語言寫的程式碼進行互動,簡單的說,一種在Java虛擬機器控制下執行程式碼的標準機制。 可以用它實現java和c語言互調。對於初學者來講,很容易吧jni和ndk的概念搞混淆(當然也可能只有博主一個人o(╯□╰)o),那jni和ndk的區別到底是什麼?
NDK是什麼
Android NDK(Native Development Kit )是一套工具集合,允許你用像C/C++語言那樣實現應用程式的一部分。 簡單的說,NDK其實多了一個把.so和.apk打包的工具,而JNI開發並沒有打包,只是把.so檔案放到檔案系統的特定位置。可以將NDK看做是Google提供的一個打包工具,方便開發者使用,有了這個工具,我們只需要關注程式碼的具體實現,而不需要關注如何編譯動態連結庫。
上手之前
先看看jni中的資料型別:
函式操作(只列出了一些常用的):
函式 | Java資料型別 | 本地型別 | 函式說明 |
---|---|---|---|
GetBooleanArrayElements | boolean | jboolean | 需要呼叫ReleaseBooleanArrayElements 釋放 |
GetByteArrayElements | byte | jbyte | 需要呼叫ReleaseByteArrayElements 釋放 |
GetCharArrayElements | char | jchar | 需要呼叫ReleaseShortArrayElements 釋 |
GetObjectArrayElement | 自定義物件 | jobject | |
SetObjectArrayElement | 自定義物件 | jobject | |
NewArray | 建立一個指定長度的原始資料型別的陣列 | ||
NewStringUTF | jstring型別的方法轉換 | ||
DeleteLocalRef | 刪除 localRef所指向的區域性引用 | ||
DeleteGlobalRef | 刪除 globalRef 所指向的全域性引用 | ||
GetMethodID | 返回類或介面例項(非靜態)方法的方法 ID。方法可在某個 clazz 的超類中定義,也可從 clazz 繼承。該方法由其名稱和簽名決定。 GetMethodID() 可使未初始化的類初始化。要獲得建構函式的方法 ID,應將 作為方法名,同時將void (V) 作為返回型別。 | ||
GetStaticMethodID | 呼叫靜態方法 | ||
CallVoidMethod | 呼叫例項方法 | ||
CallMethod |
天才第一步,Hello World來一個
首先得有ndk的環境,環境配置很簡單,博主就不在這裡演示了。直接新建一個工程,勾選上c++支援:
然後看看Android Studio給我們生成了什麼:
#####初識cmake
- cmake是什麼:脫離 Android 開發來看,c/c++ 的編譯檔案在不同平臺是不一樣的。Unix 下會使用
makefile
檔案編譯,Windows 下會使用project
檔案編譯。而CMake
則是一個跨平臺的編譯工具,它並不會直接編譯出物件,而是根據自定義的語言規則(CMakeLists.txt
)生成 對應makefile
或project
檔案,然後再呼叫底層的編譯。 - 和ndk的區別:在 Android Studio 2.2 之後你有2種選擇來編譯你寫的 c/c++ 程式碼。一個是
ndk-build
+Android.mk
+Application.mk
組合,另一個是CMake
+CMakeLists.txt
組合。這2個組合與Android程式碼和c/c++程式碼無關,只是不同的構建指令碼和構建命令。說白了,cmake就是ndk的替代者。
本文使用的是後者即cmake構建,這也是google官方主推的。
cmake工程和普通的工程相比就多了這三個地方,一個是CMakeLists.txt檔案,檔案內容如下:
cmake_minimum_required(VERSION 3.4.1)
add_library( # 生成的so庫名稱,此處生成的so檔名稱是libnative-lib.so
native-lib
# SHARED是動態庫,會被動態連結,在執行時被載入
# STATIC:靜態庫,是目標檔案的歸檔檔案,在連結其它目標的時候使用
# MODULE:模組庫,是不會被連結到其它目標中的外掛
SHARED
# 資源路徑是相對路徑,相對於本CMakeLists.txt所在目錄
src/main/cpp/native-lib.cpp )
# 從系統查詢依賴庫
find_library( # android系統每個型別的庫會存放一個特定的位置,而log庫存放在log-lib中
log-lib
# android系統在c環境下打log到logcat的庫
log )
# 配置庫的連結(依賴關係)
target_link_libraries( # 目標庫
native-lib
# 依賴於
${log-lib} )
複製程式碼
註釋寫的很明確了,對於初學者,只需要注意的兩個地方是,第一處和第三處的名字必須是相同的,第二處只要你在cpp資料夾下新建了.cpp檔案,都需要在這裡申明一下,是不是有點像清單檔案的感覺。
關於cmake的具體使用,網上有很多教程,博主就不多說了。
cpp檔案分析
然後就是.cpp檔案裡的內容了:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring
JNICALL
Java_com_ndk_lingxiao_ndkproject_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
複製程式碼
一個一個分析。
- 首先前兩句是標頭檔案,沒什麼好說的。
- extern "C"主要作用就是為了能夠正確實現C++程式碼呼叫其他C語言程式碼 ,也就是相容c語言。
- JNIEXPORT 在Jni程式設計中所有本地語言實現Jni介面的方法前面都有一個"JNIEXPORT",這個可以看做是Jni的一個標誌,表示此函式是被jni呼叫的
- jstring 返回值型別是string型別的
- JNICALL 這個可以理解為Jni 和Call兩個部分,和起來的意思就是 Jni呼叫XXX(後面的XXX就是JAVA的方法名)
- Java_com_ndk_lingxiao_ndkproject_MainActivity_stringFromJNI,別看這玩意兒這麼長,他就是嚇唬你的,我相信人有所長,你一定比他長,不要被嚇到[]~( ̄▽ ̄)~*。固定寫法Java_+類名全路徑+方法名,只是把類名的“.”替換為了下劃線""。很簡單的有木有。
- JNIEnv * env:這個env可以看做是Jni介面本身的一個物件,jni.h標頭檔案中存在著大量被封裝好的函式,這些函式也是Jni程式設計中經常被使用到的,要想呼叫這些函式就需要使用JNIEnv這個物件。例如:env->GetObjectClass()。
- jobject obj 有兩種情況,一種是可以看做Java類的一個例項化物件 ,如Hello hello = new Hello(),hello.method(),這時候的obj 就是hello。哎,一不小心又new了一個物件出來。一種是可以看做是java類的本身 ,如果method是靜態方法,它不是屬於一個物件的,而是屬於一個類的 ,這時候就代表Hello.class。
- std::string hello = "Hello from C++" 相當於stirng str = "Hello from C++",但是c++的字串和java的字串不一樣,所以需要轉換一下再返回,所以通過env物件呼叫方法轉換為java能識別的env->NewStringUTF(hello.c_str())
cpp檔案也講完了,現在看看MainActivity裡的程式碼:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
public static native String stringFromJNI();
}
複製程式碼
只需要將一下那個靜態程式碼塊,loadLibrary的時候,本來生成的.so檔案為libnative-lib.so但是這裡沒有加是android studio會自動給我們加上去,如果這裡再加上就會重複,所以只需要填寫和CMakeLists.txt裡的命名相同就行了。
c語言裡列印Log
首先在module級的build.gradle里加入:
defaultConfig {
ndk{
ldLibs "gomp"
}
}
複製程式碼
然後在cpp中加入如下的巨集定義:
#include <android/log.h>
#define LOG_TAG "NATIVE_LIB"
#define DEBUG
#define ANDROID_PLATFORM
#ifdef DEBUG
#ifdef ANDROID_PLATFORM
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
#else
#define LOGD(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#define LOGI(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#define LOGW(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#define LOGE(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#endif
#else
#define LOGD(...)
#define LOGI(...)
#define LOGW(...)
#define LOGE(...)
#endif
複製程式碼
搞定,這個是固定寫法,沒什麼好說的。
java呼叫C++方法
這個比較簡單,這裡就隨便提一下,首先我新建了一個Hello類,寫了兩個方法,android studio會提示是否生成方法:
生成方法之後我只加了兩句列印:
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callStaticMethod(JNIEnv *env, jclass type, jint i) {
LOGD("im from static moethod C++ , value is : %d",i);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callInstanceMethod
(JNIEnv *, jobject, jint i){
LOGD("im from instance moethod C++ , value is : %d",i);
}
複製程式碼
然後在相應的地方呼叫一下,我是在MainActivity中呼叫的:
然後看一下後面的重點,c++中呼叫java層的方法和修改java層的屬性。
方法簽名
在學習c++呼叫java方法時需要了解的是方法簽名,關於方法簽名,我覺得只要關注這兩個地方就行了:
- 什麼是方法簽名:方法簽名由方法名稱和一個引數列表(方法的引數的順序和型別)組成。
- 為什麼要用方法簽名:c語言中沒有方法過載這個概念,如果java中有兩個方法:long test(int n, String str, int[] arr) ,long test(String str) 。那麼沒有方法簽名來標註一下,編譯器不就懵逼了嘛(ノ`Д)ノ。
下面有請方法簽名規則表開始表演:
Java型別 | 簽名型別 |
---|---|
boolean | Z |
byte | B |
char | C |
long | J |
float | F |
double | D |
short | S |
int | I |
類 | L全限定類名 |
陣列 | [全限定類名 |
上述中類的簽名規則是:”L+全限定類名+;”三部分組成,其中全限定類名以”/”分隔,而不是用”.”或”_”分隔。
比如剛剛說的那兩個方法:
- long test(String str) :方法簽名為(Ljava/lang/String;)J ,括號裡的內容代表string括號後面是返回值型別簽名,J代表long型。
- long test(int n, String str, int[] arr) :其方法簽名為(ILjava/lang/String;[I)J括號裡的內容分成三部分,之間沒有空格,即”I”,”Ljava/lang/String;”和”[I”,分別代表int,String,int[]
有迷妹私信我了:這麼複雜的嗎?有沒有簡單快捷的方法,每次都這麼麻煩,太浪費時間了吧!我的時間很寶貴的嚶嚶嚶,要是沒有我砍死你
很大方的(迫不得已)交出偷懶方法:
javap -s 類的.class路徑
複製程式碼
可以說是很直觀了(逃),博主用的as3.1,所以這個目錄在工程,目錄\module目錄\build\intermediates\classes\debug下面。得到方法簽名之後,就可以開始下面的操作了
C++呼叫Java靜態方法
在java中寫了一個這樣的方法:
public static void staticMethod(String data){
logMessage(data);
}
public static void logMessage(String data){
Log.d("hello", data);
}
複製程式碼
我希望在cpp中呼叫staticMethod方法,該怎麼做呢?先貼程式碼:
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callJavaStaticMethod(JNIEnv *env, jclass type) {
jclass clazz = NULL;
jmethodID method_id = NULL;
jstring str_log = NULL;
clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
if (clazz == NULL){
LOGD("沒有發現該類");
return;
}
method_id = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;)V");
if (method_id == NULL){
LOGD("沒有發現該方法名");
return;
}
str_log = env->NewStringUTF("c++ 呼叫java的靜態方法");
env->CallStaticVoidMethod(clazz,method_id,str_log);
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(str_log);
return ;
}
複製程式碼
這裡如果對jvm虛擬機器比較瞭解的同學可能會更容易理解,博主正在瞭解中,所以假裝解釋一波,只是按照我自己的理解,來解釋,可能後面會改動(~ ̄▽ ̄)~ 。
首先定義了三個變數,然後使用env呼叫封裝好的方法FindClass,傳入類名全路徑,在jvm中如果有載入這個類,那麼就會返回我們的這個類。
接著是獲取方法的id,使用env呼叫GetStaticMethodID,第一個引數是方法所在的類,第二個是方法名,第三個是方法簽名。
然後使用env呼叫CallStaticVoidMethod,傳入類和方法和引數,完成對java層方法的呼叫。
最後不要忘記刪除引用,不然會發生記憶體洩漏。
C++呼叫Java例項方法
和靜態方法的區別就兩個地方,一個是GetStaticMethodID,一個是CallStaticVoidMethod:
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callJavaInstanceMethod(JNIEnv *env, jobject instance) {
jclass clazz = NULL;
jmethodID method_id = NULL;
jstring str_log = NULL;
clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
if (clazz == NULL){
LOGD("沒有發現該類");
return;
}
method_id = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;)V");
if (method_id == NULL){
LOGD("沒有發現該方法名");
return;
}
str_log = env->NewStringUTF("c++ 呼叫java的例項方法");
env->CallVoidMethod(instance,method_id,str_log); //clazz 改為instance
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(str_log);
return ;
}
複製程式碼
C++呼叫Java變數
首先在java類中定義一個變數:
public String name = "im is java";
複製程式碼
然後貼上jni程式碼,主要方法是GetFieldID,第一個引數傳入變數所在類,第二個引數是變數名,第三個引數是簽名型別:
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_changeField(JNIEnv *env, jobject instance) {
jclass clazz = env->GetObjectClass(instance);
if (clazz == NULL){
return;
}
jfieldID jfieldID = env->GetFieldID(clazz,"name","Ljava/lang/String;");
if (jfieldID == NULL){
return;
}
jstring obj_str = (jstring) env->GetObjectField(instance,jfieldID);
if (obj_str == NULL){
return;
}
char* c_str = (char*) env->GetStringUTFChars(obj_str,JNI_FALSE);
const char new_char[40] = "changed from c";
//複製new_char的內容到c_str
strcpy(c_str,new_char);
jstring new_str = env->NewStringUTF(c_str);
LOGD("%s",new_char);
env->SetObjectField(instance,jfieldID,new_str);
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(obj_str);
env->DeleteLocalRef(new_str);
return;
}
複製程式碼
C++呼叫Java靜態變數
同理,靜態變數也沒啥好講的了,這裡就貼一下程式碼:
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_changeStaticField(JNIEnv *env, jclass type) {
jclass clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
if (clazz == NULL){
return;
}
jfieldID jfieldID = env->GetStaticFieldID(clazz,"age","I");
if (jfieldID == NULL){
return;
}
int age = env->GetStaticIntField(clazz,jfieldID);
LOGD("%d",age);
jint change_int = 12;
env->SetStaticIntField(clazz,jfieldID,change_int);
env->DeleteLocalRef(clazz);
}
複製程式碼
學習JNI,個人建議是在平常的工作中能用到的才去深入學習,因為這個東西只有實踐才有意義。關於如何在native中排查錯誤,可以使用ndk-stack工具,使用方法賊簡單,一個命令列的事兒,這裡就不說了。
本文demo的github地址:NdkDemo
參考連結: