NDK 基礎知識–JNI
開發環境: Android studio v3.6.1
(3.6.0都支持kotlin與c/c++互相調用,是該學學NDK了,不能再找理由了)
NDK 可以讓我們Android 應用中使用C、C++代碼。以前Android 都是使用java,NDK中包含JNI (java本地接口)可以使用java 調用c、c++等。如今kotlin被Android 官方宣佈第一開發語言。kotlin與java是100%兼容的(我認爲kotlin、java都依靠jvm,他們都要編譯成java字節碼,kotlin只是利用它的編譯器特性,簡化了java語法。這應該就是以後編程語言發展趨勢吧,讓我們少做點,電腦多做的)
好了,廢話不說了,正片開始
1.native方法
在java 文件中聲明一個native方法
//Test.java
package com.wkk.ndkdemo;
public class Test {
//聲明native方法,不用實現,方法實現代碼在c或c++中
native void test();
}
如果是kotlin 則是external
關鍵詞
external fun test()
c++ 文件
#include <jni.h>
extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_Test_test(JNIEnv *env, jobject thiz) {
}
這裏c++的方法名特麼長Java_com_wkk_ndkdemo_Test_test
這個名字是有固定語法的
方法的第一個參數是JNIEnv
指針, JNIEnv 是一個結構體,定義了許多與java的方法。
方法的第二個參數jobject
表示調用test()方法的對象。test()方法是成員方法,如果是個靜態方法,則第二個參數就是jclass
表示當前方法的class ,因爲靜態方法屬於類。
上面看到jobject 類型對應java中Object ,jni定義一寫類型和java類型對應起來,如下
//都是java基本類型前面加個j
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */ //---->java 中boolean
typedef int8_t jbyte; /* signed 8 bits */ //---->java byte
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
typedef _jobject* jobject; // 對應java中的類對象
typedef _jclass* jclass; // 對應java中的clas
typedef _jstring* jstring;//對應java中的string
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
…… 省略更多請看jni.h文件中的定義
類如在java中參數爲Int類型的方法
native void test1(int number);
則對應的c++
extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_Test_test1(JNIEnv *env, jobject thiz, jint number) {
}
可以看到在java中int類型參數,在C++中對應的是jint方法
2.獲取java中的屬性和方法(類似java反射)
public class User {
private String name="Jack";
private int age=20;
public String getName() {
Log.i("User","who在調用我");
return name;
}
public void setName(String name) {
Log.i("User","啊!我被人在調用了");
this.name = name;
}
public native void test();
}
c++代碼
extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_User_test(JNIEnv *env, jobject user) {
//通過對象獲取類jclass
jclass userCls = env->GetObjectClass(user);
//———————————————獲取屬性值———————————————————
//獲取屬性id 0️⃣
jfieldID ageId = env->GetFieldID(userCls, "age", "I");
//通過屬性id獲取屬性值
jint ageValue = env->GetIntField(user, ageId);
//要想在logcat看到值需要引入android/log.h頭文件,使用下面方法打印
__android_log_print(ANDROID_LOG_INFO,"User","c++,ageValue%d",ageValue);
//-----------------------
//同理獲取name屬性值 1️⃣
jfieldID nameId = env->GetFieldID(userCls, "name", "Ljava/lang/String;");
//通過屬性id獲取屬性值 String屬性
jstring nameValue = static_cast<jstring>(env->GetObjectField(user, nameId));
//———————————————調用方法,無參數———————————————————
//獲取方法id /2️⃣3️⃣4️⃣5️⃣6️⃣
jmethodID getNameId = env->GetMethodID(userCls, "getName", "()Ljava/lang/String;");
//調用getName方法
jobject callGetName = env->CallObjectMethod(user, getNameId);
//———————————————調用方法,有參數———————————————————
//獲取方法id3️⃣
jmethodID setNameId = env->GetMethodID(userCls, "setName", "(Ljava/lang/String;)V");
//調用getName方法4️⃣
env->CallVoidMethod(user, setNameId, env->NewStringUTF("abc"));
//———————————————創建新對象———————————————————
//獲取構造方法id5️⃣
jmethodID initID = env->GetMethodID(userCls, "<init>", "()V");
//通過c++代碼創建user對象
jobject newUser = env->NewObject(userCls, initID);
//c/c++沒有垃圾回收機制,需要自己釋放內存
env->DeleteLocalRef(newUser);
}
在0️⃣ 使用env->GetFieldID方法獲取屬性id,此方法需要三個參數,第一個是jclass,第二個屬性名,第三個參數寫了個"I"
表示int類型簽名,對應關係如下表所示,最新版的Android studio 已經有自動補全功能,當你填寫第二個屬性時,編輯器會自動補全第三個參數類型簽名
java | 簽名 |
---|---|
int | I |
char | C |
byte | B |
long | L |
float | F |
boolean | Z |
String(類) | Ljava/lang/String; |
自己寫的類 | L類的全路徑; |
例如com.wkk.ndkdemo包下面有Data.java 則它的簽名爲Lcom/wkk/ndkdemo/Data;
在2️⃣處使用env->GetMethodID獲取某個方法的id,最後一個參數爲方法簽名,就是把原方法用類型簽名表示,如上面的String getName()
無參數,返回數據類型爲String,所以方法簽名是()Ljava/lang/String;
在3️⃣出 setName 有參數,參數是String類型,無返回值,所以方法簽名爲(Ljava/lang/String;)V
返回值爲viod 用V表示。
在4️⃣ ,通過CallVoidMethod方法調用setName方法,調用java方法都是CallXXXMethod ,xxx表示方法的返回值類型。此類方法前兩個參數分別是jobject,jmethodID,最後一個爲可以變參數。被調用的Java方法參數,setName的參數是String類型,因爲是Java方法,要通過NewStringUTF把字符串轉換爲java可以使用的類型。
我們在c++中可以創建java對象,在5️⃣,調用類的構造方法,每個類的構造方法名都是“"” 並不是像java那樣和類名一樣。通過NewObject創建對象,最後一個參數也是可變參數,添加構造方法的參數值
3. JNI_OnLoad、動態註冊
上面忘記說了要想使用c++需要加載C++庫
在靜態代碼塊中調用
static {
System.loadLibrary("native-lib")
}
如果是kotlin
companion object{
init {
System.loadLibrary("native-lib")
}
}
當調用System.loadLibrary()
·方法加載庫時,如果庫中有jint JNI_OnLoad(JavaVM* vm, void* reserved);
方法就會先執行這個方法
上面介紹java方法與C++方法關聯的方式一般稱爲靜態註冊。還有一種動態註冊,就是利用JNI_OnLoad 實現。
java 代碼
package com.wkk.jnidemo;
public class Data {
public native int test();
public native int test2(int a);
}
c++代碼
#include <jni.h>
//如果用不到JNIEnv jobject 兩個參數可以省略不寫
jint test(JNIEnv *env, jobject data) {
return 10;
}
jint test2(JNIEnv *env, jobject data, jint a) {
return 1 + a;
}
static const char *CLASS_NAME = "com/wkk/jnidemo/Data";
//JNINativeMethod 是一個結構體
static const JNINativeMethod methods[] = {
{
"test",//java中的方法名
"()I",//對應的方法簽名
(void *) test//c++中對應的函數指針 轉化爲void 指針
},
{
"test2",
"(I)I",
(void *) test2
}
};
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
//獲取JNIEnv指針
jint status = vm->GetEnv((void **) &env, JNI_VERSION_1_6);
if (status != JNI_OK) {
//如果獲取失敗 return -1 結束
return -1;
}
//通過env的FindClass方法通過類名獲取jclass
jclass dataClass = env->FindClass(CLASS_NAME);
//註冊方法,把java native方法與c++方法關聯
//第一個參數爲對應的jclass
//第二個參數是JNINativeMethod 數據組
//第三個參數表示註冊方法的個數
jint registerNativesStatus=env->RegisterNatives(dataClass,methods, sizeof(methods)/ sizeof(JNINativeMethod));
if (registerNativesStatus != JNI_OK) {
return -1;
}
return JNI_VERSION_1_6;
}
以上就是動態註冊的簡單過程。
文章如有解釋不對的地方,往大佬指點