注:原文地址
緊接上篇:Android NDK開發(fā):JNI基礎(chǔ)篇 | cfanr,這篇主要介紹 JNI Native 層調(diào)用Java 層的代碼(涉及JNI 數(shù)據(jù)類型映射和描述符的使用)和如何動態(tài)注冊 JNI。
1. Hello World NDK
在開始實戰(zhàn)練習(xí)前,你需要先大致了解運行一個 Hello World 的項目大概需要做什么,有哪些配置以及配置的具體意思。 Android Studio(2.2以上版本)提供兩種方式編譯原生庫:CMake( 默認(rèn)方式) 和 ndk-build。對于初學(xué)者可以先了解 CMake 的方式,另外,對于本文可以暫時不用了解 so 庫如何編譯和使用。
一個 Hello World 的 NDK 項目很簡單,按照流程新建一個 native 庫工程就可以,由于太簡單,而且網(wǎng)上也有很多教程,這里就沒必要浪費時間再用圖文介紹了。詳細(xì)操作方法,可以參考這篇文章,AS2.2使用CMake方式進(jìn)行JNI/NDK開發(fā)-于連林- CSDN博客
列出項目中涉及 NDK 的內(nèi)容或配置幾點需要注意的地方:
- .externalNativeBuild 文件是 CMake 編譯好的文件,顯示支持的各種硬件平臺的信息,如 ARM、x86等;
- cpp 文件是放置 native 文件的地方,名字可以修改成其他的(只要里面函數(shù)名字對應(yīng)Java native 方法就好);
- CMakeLists.txt,AS自動生成的 CMake 腳本配置文件
# value of 3.4.0 or lower.
# 1.指定cmake版本
cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library. ——>2.生成函數(shù)庫的名字,需要寫到程序中的 so 庫的名字
native-lib
# Sets the library as a shared library. 生成動態(tài)函數(shù)
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included. ——> 依賴的cpp文件,每添加一個 C/C++文件都要添加到這里,不然不會被編譯
src/main/cpp/native-lib.cpp
)
find_library( # Sets the name of the path variable. 設(shè)置path變量的名稱
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate. #指定要查詢庫的名字
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in the
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library. 目標(biāo)庫, 和上面生成的函數(shù)庫名字一致
native-lib
# Links the target library to the log library
# included in the NDK. 連接的庫,根據(jù)log-lib變量對應(yīng)liblog.so函數(shù)庫
${log-lib} )
build.gradle 文件,注意兩個 externalNativeBuild {}的配置
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "cn.cfanr.jnisample"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags "" //如果使用 C++11 標(biāo)準(zhǔn),則改為 "-std=c++11"
// 生成.so庫的目標(biāo)平臺,使用的是genymotion模擬器,需要加上 x86
abiFilters "armeabi-v7a", "armeabi", "x86"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt" //配置 CMake 文件的路徑
}
}
}
local.properties 文件會多了 ndk 路徑的配置
ndk.dir=/Users/cfanr/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/cfanr/Library/Android/sdk
MainActivity 調(diào)用 so 庫
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
//……
}
另外,還需要回顧上篇 JNI 基礎(chǔ)篇-靜態(tài)注冊也提到 JNI 的函數(shù)命名規(guī)則:JNIEXPORT 返回值 JNICALL Java_全路徑類名_方法名_參數(shù)簽名(JNIEnv* , jclass, 其它參數(shù));其中第二個參數(shù),當(dāng) java native 方法是 static 時,為 jclass,當(dāng)為非靜態(tài)方法時,為 jobject,為了簡單起見,下面的例子 JNI 函數(shù)都標(biāo)記extern "C",函數(shù)名就不需要寫參數(shù)簽名了
2. JNI 函數(shù)訪問 Java 對象的變量
注:以下練習(xí)中 Java native 方法都是非靜態(tài)的
步驟:
- 1)通過
env->GetObjectClass(jobject)獲取Java 對象的 class 類,返回一個 jclass; - 2)調(diào)用
env->GetFieldID(jclazz, fieldName, signature)得到該實例域(變量)的 id,即 jfieldID;如果變量是靜態(tài) static 的,則調(diào)用的方法為GetStaticFieldID - 3)最后通過調(diào)用
env->Get{type}Field(jobject, fieldId)得到該變量的值。其中{type} 是變量的類型;如果變量是靜態(tài) static 的,則調(diào)用的方法是GetStatic{type}Field(jclass, fieldId),注意 static 的話, 是使用 jclass 作為參數(shù);
2.1 訪問某個變量,并通過某個方法對其處理后返回
native方法定義和調(diào)用
public int num = 10;
public native int addNum();
FieldJni fieldJni = new FieldJni();
Log.e(TAG, "調(diào)用前:num = " + fieldJni.num);
Log.e(TAG, "調(diào)用后:" + fieldJni.addNum());
C++層:
extern "C"
JNIEXPORT jint JNICALL
Java_cn_cfanr_jnisample_FieldJni_addNum(JNIEnv *env, jobject jobj) {
//獲取實例對應(yīng)的 class
jclass jclazz = env->GetObjectClass(jobj);
//通過class獲取相應(yīng)的變量的 field id
jfieldID fid = env->GetFieldID(jclazz, "num", "I");
//通過 field id 獲取對應(yīng)的值
jint num = env->GetIntField(jobj, fid); //注意,不是用 jclazz, 使用 jobj
num++;
return num;
}
輸出結(jié)果:
MainActivity: 調(diào)用前:num = 10
MainActivity: 調(diào)用后:11
由于 jclass 也是繼承 jobject,所以使用 GetIntField 時不要混淆兩個參數(shù)
2.2 訪問一個 static 變量,并對其修改
native方法定義和調(diào)用
public static String name = "cfanr";
public native void accessStaticField();
//調(diào)用
Log.e(TAG, "調(diào)用前:name = " + fieldJni.name);
fieldJni.accessStaticField();
Log.e(TAG, "調(diào)用后:" + fieldJni.name);
C++代碼:
extern "C"
JNIEXPORT void JNICALL
Java_cn_cfanr_jnisample_FieldJni_accessStaticField(JNIEnv *env, jobject jobj) {
jclass jclazz = env->GetObjectClass(jobj);
jfieldID fid = env->GetStaticFieldID(jclazz, "name", "Ljava/lang/String;"); //注意是用GetStaticFieldID,不是GetFieldID
jstring name = (jstring) env->GetStaticObjectField(jclazz, fid);
const char* str = env->GetStringUTFChars(name, JNI_FALSE);
/*
* 不要用 == 比較字符串
* name == (jstring) "cfanr"
* 或用 = 直接賦值
* name = (jstring) "navy"
* 警告:warning: result of comparison against a string literal is unspecified (use strncmp instead) [-Wstring-compare]
*/
char ch[30] = "hello, ";
strcat(ch, str);
jstring new_str = env->NewStringUTF(ch);
// 將jstring類型的變量,設(shè)置到j(luò)ava
env->SetStaticObjectField(jclazz, fid, new_str);
}
輸出結(jié)果:
MainActivity: 調(diào)用前:name = cfanr
MainActivity: 調(diào)用后:hello, cfanr
需要注意的是,獲取 java 靜態(tài)變量,都是調(diào)用 JNI 相應(yīng)靜態(tài)的函數(shù),不能調(diào)用非靜態(tài)的,同時留意傳入的參數(shù)是 jclass,而不是 jobject
2.3 訪問一個 private 變量,并對其修改
native方法定義和調(diào)用
private int age = 21;
public native void accessPrivateField();
public int getAge() {
return age;
}
//調(diào)用
Log.e(TAG, "調(diào)用前:age = " + fieldJni.getAge());
fieldJni.accessPrivateField();
Log.e(TAG, "調(diào)用后:age = " + fieldJni.getAge());
C++:
extern "C"
JNIEXPORT void JNICALL
Java_cn_cfanr_jnisample_FieldJni_accessPrivateField(JNIEnv *env, jobject jobj) {
jclass clazz = env->GetObjectClass(jobj);
jfieldID fid = env->GetFieldID(clazz, "age", "I");
jint age = env->GetIntField(jobj, fid);
if(age > 18) {
age = 18;
} else {
age--;
}
env->SetIntField(jobj, fid, age);
}
輸出結(jié)果:
MainActivity: 調(diào)用前:age = 21
MainActivity: 調(diào)用后:age = 18
3. JNI 函數(shù)調(diào)用 Java 對象的方法
步驟:(和訪問 Java 對象的變量有點類型)
- 1)通過
env->GetObjectClass(jobject)獲取Java 對象的 class 類,返回一個 jclass; - 2)通過
env->GetMethodID(jclass, methodName, sign)獲取到 Java 對象的方法 Id,即 jmethodID,當(dāng)獲取的方法是 static 的時,使用GetStaticMethodID; - 3)通過 JNI 函數(shù)
env->Call{type}Method(jobject, jmethod, param...)實現(xiàn)調(diào)用 Java的方法;若調(diào)用的是 static 方法,則使用CallStatic{type}Method(jclass, jmethod, param...),使用的是 jclass
3.1 調(diào)用 Java 公有方法
native方法定義和調(diào)用
private String sex = "female";
public void setSex(String sex) {
this.sex = sex;
}
public String getSex(){
return sex;
}
public native void accessPublicMethod();
//調(diào)用
MethodJni methodJni = new MethodJni();
Log.e(TAG, "調(diào)用前:getSex() = " + methodJni.getSex());
methodJni.accessPublicMethod();
Log.e(TAG, "調(diào)用后:getSex() = " + methodJni.getSex());
C++
extern "C"
JNIEXPORT void JNICALL
Java_cn_cfanr_jnisample_MethodJni_accessPublicMethod(JNIEnv *env, jobject jobj) {
//1.獲取對應(yīng) class 的實體類
jclass jclazz = env->GetObjectClass(jobj);
//2.獲取方法的 id
jmethodID mid = env->GetMethodID(jclazz, "setSex", "(Ljava/lang/String;)V");
//3.字符數(shù)組轉(zhuǎn)換為字符串
char c[10] = "male";
jstring jsex = env->NewStringUTF(c);
//4.通過該 class 調(diào)用對應(yīng)的 public 方法
env->CallVoidMethod(jobj, mid, jsex);
}
結(jié)果:
MainActivity: 調(diào)用前:getSex() = female
MainActivity: 調(diào)用后:getSex() = male
調(diào)用 java private 方法也是類似, Java 的訪問域修飾符對 C++無效
3.2 調(diào)用 Java 靜態(tài)方法
native方法定義和調(diào)用
private static int height = 170;
public static int getHeight() {
return height;
}
public native int accessStaticMethod();
//調(diào)用
Log.e(TAG, "調(diào)用靜態(tài)方法:getHeight() = " + methodJni.accessStaticMethod());
C++
extern "C"
JNIEXPORT jint JNICALL
Java_cn_cfanr_jnisample_MethodJni_accessStaticMethod(JNIEnv *env, jobject jobj) {
//1.獲取對應(yīng) class 實體類
jclass jclazz = env->GetObjectClass(jobj);
//2.通過 class 類找到對應(yīng)的方法 id
jmethodID mid = env->GetStaticMethodID(jclazz, "getHeight", "()I"); //注意靜態(tài)方法是調(diào)用GetStaticMethodID, 不是GetMethodID
//3.通過 class 調(diào)用對應(yīng)的靜態(tài)方法
return env->CallStaticIntMethod(jclazz, mid);
}
輸出結(jié)果:
MainActivity: 調(diào)用靜態(tài)方法:getHeight() = 170
注意調(diào)用的靜態(tài)方法要一致。
3.3 調(diào)用 Java 父類方法
native方法定義和調(diào)用
public class SuperJni {
public String hello(String name) {
return "welcome to JNI world, " + name;
}
}
public class MethodJni extends SuperJni{
public native String accessSuperMethod();
}
//調(diào)用
Log.e(TAG, "調(diào)用父類方法:hello(name) = " + methodJni.accessSuperMethod());
C++
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_cfanr_jnisample_MethodJni_accessSuperMethod(JNIEnv *env, jobject jobj) {
//1.通過反射獲取 class 實體類
jclass jclazz = env-> FindClass("cn/cfanr/jnisample/SuperJni"); //注意 FindClass 不要 L和;
if(jclazz == NULL) {
char c[10] = "error";
return env->NewStringUTF(c);
}
//通過 class 找到對應(yīng)的方法 id
jmethodID mid = env->GetMethodID(jclazz, "hello", "(Ljava/lang/String;)Ljava/lang/String;");
char ch[10] = "cfanr";
jstring jstr = env->NewStringUTF(ch);
return (jstring) env->CallNonvirtualObjectMethod(jobj, jclazz, mid, jstr);
}
注意兩點不同的地方,
- 獲取的是父類的方法,所以不能通過GetObjectClass獲取,需要通過反射 FindClass 獲?。?/li>
- 調(diào)用父類的方法是 CallNonvirtual{type}Method 函數(shù)。Nonvirtual是非虛擬函數(shù)
4. Java 方法傳遞參數(shù)給 JNI 函數(shù)
native 方法既可以傳遞基本類型參數(shù)給 JNI(可以不經(jīng)過轉(zhuǎn)換直接使用),也可以傳遞復(fù)雜的類型(需要轉(zhuǎn)換為 C/C++ 的數(shù)據(jù)結(jié)構(gòu)才能使用),如數(shù)組,String 或自定義的類等。
基礎(chǔ)類型,這里就不舉例子了,詳細(xì)可以看 GitHub 上的源碼: AndroidTrainingDemo/JNISample
要用到的 JNI 函數(shù):
- 獲取數(shù)組長度:
GetArrayLength(j{type}Array),type 為基礎(chǔ)類型; - 數(shù)組轉(zhuǎn)換為對應(yīng)類型的指針:
Get{type}ArrayElements(jarr, 0) - 獲取構(gòu)造函數(shù)的 jmethodID 時,仍然是用
env->GetMethodID(jclass, methodName, sign)獲取,方法 name 是<init>; - 通過構(gòu)造函數(shù) new 一個 jobject,
env->NewObject(jclass, constructorMethodID, param...),無參構(gòu)造函數(shù) param 則為空
4.1 數(shù)組參數(shù)的傳遞
計算整型數(shù)組參數(shù)的和
native方法定義和調(diào)用
public native int intArrayMethod(int[] arr);
//調(diào)用
ParamsJni paramsJni = new ParamsJni();
Log.e(TAG, "intArrayMethod: " + paramsJni.intArrayMethod(new int[]{4, 9, 10, 16})+"");
C++
extern "C"
JNIEXPORT jint JNICALL
Java_cn_cfanr_jnisample_ParamsJni_intArrayMethod(JNIEnv *env, jobject jobj, jintArray arr_) {
jint len = 0, sum = 0;
jint *arr = env->GetIntArrayElements(arr_, 0);
len = env->GetArrayLength(arr_);
//由于一些版本不兼容,i不定義在for循環(huán)中
jint i=0;
for(; i < len; i++) {
sum += arr[i];
}
env->ReleaseIntArrayElements(arr_, arr, 0); //釋放內(nèi)存
return sum;
}
輸出結(jié)果:
MainActivity: intArrayMethod: 39
4.2 自定義對象參數(shù)的傳遞
Person 定義,native方法定義和調(diào)用
public class Person {
private String name;
private int age;
public Person() {
}
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person :{ name: "+name+", age: "+age+"}";
}
}
//傳遞復(fù)雜對象person,再jni函數(shù)中新構(gòu)造一個person傳回java層輸出
public native Person objectMethod(Person person);
//調(diào)用
Log.e(TAG, "objectMethod: " + paramsJni.objectMethod(new Person()).toString() + "");
C++:
extern "C"
JNIEXPORT jobject JNICALL
Java_cn_cfanr_jnisample_ParamsJni_objectMethod(JNIEnv *env, jobject jobj, jobject person) {
jclass clazz = env->GetObjectClass(person); //注意是用 person,不是 jobj
// jclass jclazz = env->FindClass("cn/cfanr/jnisample/model/Person;"); //或者通過反射獲取
if(clazz == NULL) {
return env->NewStringUTF("cannot find class");
}
//獲取方法 id
jmethodID constructorMid = env->GetMethodID(clazz, "<init>", "(ILjava/lang/String;)V");
if(constructorMid == NULL) {
return env->NewStringUTF("not find constructor method");
}
jstring name = env->NewStringUTF("cfanr");
return env->NewObject(clazz, constructorMid, 21, name);
}
輸出結(jié)果
MainActivity: objectMethod: Person :{ name: cfanr, age: 21}
注意:傳遞對象時,獲取的 jclass 是獲取該參數(shù)對象的 jobject 獲取,而不是第二個參數(shù)(定義該 native 方法的對象)??;
4.3 自定義對象的集合參數(shù)的傳遞
native方法定義和調(diào)用
public native ArrayList<Person> personArrayListMethod(ArrayList<Person> persons);
//調(diào)用
ArrayList<Person> personList = new ArrayList<>();
Person person;
for (int i = 0; i < 3; i++) {
person = new Person();
person.setName("cfanr");
person.setAge(10 + i);
personList.add(person);
}
Log.e(TAG, "調(diào)用前:java list = " + personList.toString());
Log.e(TAG, "調(diào)用后:jni list = " + paramsJni.personArrayListMethod(personList).toString());
C++
extern "C"
JNIEXPORT jobject JNICALL
Java_cn_cfanr_jnisample_ParamsJni_personArrayListMethod(JNIEnv *env, jobject jobj, jobject persons) {
//通過參數(shù)獲取 ArrayList 對象的 class
jclass clazz = env->GetObjectClass(persons);
if(clazz == NULL) {
return env->NewStringUTF("not find class");
}
//獲取 ArrayList 無參數(shù)的構(gòu)造函數(shù)
jmethodID constructorMid = env->GetMethodID(clazz, "<init>", "()V");
if(constructorMid == NULL) {
return env->NewStringUTF("not find constructor method");
}
//new一個 ArrayList 對象
jobject arrayList = env->NewObject(clazz, constructorMid);
//獲取 ArrayList 的 add 方法的id
jmethodID addMid = env->GetMethodID(clazz, "add", "(Ljava/lang/Object;)Z");
//獲取 Person 類的 class
jclass personCls = env->FindClass("cn/cfanr/jnisample/model/Person");
//獲取 Person 的構(gòu)造函數(shù)的 id
jmethodID personMid = env->GetMethodID(personCls, "<init>", "(ILjava/lang/String;)V");
jint i=0;
for(; i < 3; i++) {
jstring name = env->NewStringUTF("Native");
jobject person = env->NewObject(personCls, personMid, 18 +i, name);
//添加 person 到 ArrayList
env->CallBooleanMethod(arrayList, addMid, person);
}
return arrayList;
}
輸出結(jié)果:
MainActivity: 調(diào)用前:java list = [Person :{ name: cfanr, age: 10}, Person :{ name: cfanr, age: 11}, Person :{ name: cfanr, age: 12}]
MainActivity: 調(diào)用后:jni list = [Person :{ name: Native, age: 18}, Person :{ name: Native, age: 19}, Person :{ name: Native, age: 20}]
復(fù)雜的集合參數(shù)也是需要通過獲取集合的 class 和對應(yīng)的方法來調(diào)用實現(xiàn)的
5. JNI 函數(shù)的字符串處理
- 訪問字符串函數(shù)
其中 isCopy 是取值為JNI_TRUE和JNI_FALSE(或者1,0),值為JNI_TRUE,表示返回JVM內(nèi)部源字符串的一份拷貝,并為新產(chǎn)生的字符串分配內(nèi)存空間。如果值為JNI_FALSE,表示返回JVM內(nèi)部源字符串的指針,意味著可以通過指針修改源字符串的內(nèi)容,不推薦這么做,因為這樣做就打破了Java字符串不能修改的規(guī)定;Java默認(rèn)使用Unicode編碼,而C/C++默認(rèn)使用UTF編碼,所以在本地代碼中操作字符串的時候,必須使用合適的JNI函數(shù)把jstring轉(zhuǎn)換成C風(fēng)格的字符串
- UTF-8字符:const char* GetStringUTFChars(jstring string, jboolean* isCopy)
- Unicode字符:const jchar* GetStringChars(jstring string, jboolean* isCopy)
- 釋放字符串內(nèi)存
- UTF-8字符:
void ReleaseStringUTFChars(jstring string, const char* utf) - Unicode 字符:
void ReleaseStringChars(jstring string, const jchar* chars)
- UTF-8字符:
- 創(chuàng)建 String 對象,UTF-8: NewStringUTF,Unicode: NewString
- 取char*的長度,UTF-8: GetStringUTFLength,Unicode: GetStringLength
- GetStringRegion和GetStringUTFRegion:分別表示獲取Unicode和UTF-8編碼字符串指定范圍內(nèi)的內(nèi)容。這對函數(shù)會把源字符串復(fù)制到一個預(yù)先分配的緩沖區(qū)內(nèi)。GetStringUTFRegion與 GetStringUTFChars 比較相似,不同的是,GetStringUTFRegion 內(nèi)部不分配內(nèi)存,不會拋出內(nèi)存溢出異常。
代碼示例就不寫了,其他詳細(xì)可參考:
JNI開發(fā)之旅(9)JNI函數(shù)字符串處理 - 貓的閣樓 - CSDN博客
JNI/NDK開發(fā)指南(四)——字符串處理 - 技術(shù)改變生活- CSDN博客
6. 動態(tài)注冊 JNI
學(xué)了上面的練習(xí),發(fā)現(xiàn)靜態(tài)注冊的方式還是挺麻煩的,生成的 JNI 函數(shù)名太長,文件、類名、變量或方法重構(gòu)時,需要重新修改頭文件或 C/C++ 內(nèi)容代碼(而且還是各個函數(shù)都要修改,沒有一個統(tǒng)一的地方),動態(tài)注冊 JNI 的方法就可以解決這個問題。
由上篇回顧下,Android NDK開發(fā):JNI基礎(chǔ)篇 | cfanr
動態(tài)注冊 JNI 的原理:直接告訴 native 方法其在JNI 中對應(yīng)函數(shù)的指針。通過使用 JNINativeMethod 結(jié)構(gòu)來保存 Java native 方法和 JNI 函數(shù)關(guān)聯(lián)關(guān)系,步驟:
- 先編寫 Java 的 native 方法;
- 編寫 JNI 函數(shù)的實現(xiàn)(函數(shù)名可以隨便命名);
- 利用結(jié)構(gòu)體 JNINativeMethod 保存Java native方法和 JNI函數(shù)的對應(yīng)關(guān)系;
- 利用
registerNatives(JNIEnv* env)注冊類的所有本地方法; - 在 JNI_OnLoad 方法中調(diào)用注冊方法;
- 在Java中通過System.loadLibrary加載完JNI動態(tài)庫之后,會自動調(diào)用JNI_OnLoad函數(shù),完成動態(tài)注冊;
代碼實例:
native 方法和調(diào)用:
public class DynamicRegisterJni {
public native String getStringFromCpp();
}
//調(diào)用
String hello = new DynamicRegisterJni().getStringFromCpp();
Log.e(TAG, hello);
C++動態(tài)注冊 JNI 代碼:
#include <jni.h>
#include "android/log.h"
#include <stdio.h>
#include <string>
#ifndef LOG_TAG
#define LOG_TAG "JNI_LOG" //Log 的 tag 名字
//定義各種類型 Log 的函數(shù)別名
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG ,__VA_ARGS__)
#endif
#ifdef __cplusplus
extern "C" {
#endif
//定義類名
static const char *className = "cn/cfanr/jnisample/DynamicRegisterJni";
//定義對應(yīng)Java native方法的 C++ 函數(shù),函數(shù)名可以隨意命名
static jstring sayHello(JNIEnv *env, jobject) {
LOGI("hello, this is native log.");
const char* hello = "Hello from C++.";
return env->NewStringUTF(hello);
}
/*
* 定義函數(shù)映射表(是一個數(shù)組,可以同時定義多個函數(shù)的映射)
* 參數(shù)1:Java 方法名
* 參數(shù)2:方法描述符,也就是簽名
* 參數(shù)3:C++定義對應(yīng) Java native方法的函數(shù)名
*/
static JNINativeMethod jni_Methods_table[] = {
{"getStringFromCpp", "()Ljava/lang/String;", (void *) sayHello},
};
//根據(jù)函數(shù)映射表注冊函數(shù)
static int registerNativeMethods(JNIEnv *env, const char *className,
const JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
LOGI("Registering %s natives\n", className);
clazz = (env)->FindClass(className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'\n", className);
return JNI_ERR;
}
if ((env)->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGE("Register natives failed for '%s'\n", className);
return JNI_ERR;
}
//刪除本地引用
(env)->DeleteLocalRef(clazz);
return JNI_OK;
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGI("call JNI_OnLoad");
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) { //判斷 JNI 版本是否為JNI_VERSION_1_4
return JNI_EVERSION;
}
registerNativeMethods(env, className, jni_Methods_table, sizeof(jni_Methods_table) / sizeof(JNINativeMethod));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
輸出結(jié)果:
JNI_LOG: call JNI_OnLoad
JNI_LOG: Registering cn/cfanr/jnisample/DynamicRegisterJni natives
JNI_LOG: hello, this is native log.
MainActivity: Hello from C++.
上面代碼涉及到 JNI 調(diào)用 Android 的 Log,只需要引入#include "android/log.h"頭文件和對函數(shù)別名命名即可。其他具體說明見上面代碼。
實際開發(fā)中可以采取動態(tài)和靜態(tài)注冊結(jié)合的方式,寫一個Java 的 native 方法完成調(diào)用動態(tài)注冊的代碼,大概代碼如下:
static {
System.loadLibrary("native-lib");
registerNatives();
}
private static native void registerNatives();
C++
JNIEXPORT void JNICALL Java_com_zhixin_jni_JniSample_registerNatives
(JNIEnv *env, jclass clazz){
(env)->RegisterNatives(clazz, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
}
7. 小結(jié)
雖然都是按照網(wǎng)上的例子做的練習(xí)記錄,但還是遇到不少小問題的,不過只要仔細(xì)查找,也比較容易發(fā)現(xiàn)問題的所在,以前覺得 JNI 挺難懂的,但這次練習(xí)下來,覺得 JNI 也只不過是一套語法規(guī)則而已,按照規(guī)則去實現(xiàn)代碼也不算特別難,當(dāng)然這只是 JNI 的一小部分內(nèi)容,JNI 還有很多內(nèi)容,如反射、異常處理、多線程、NIO 等。雖然這次練習(xí)比較簡單,但建議還是自己親自敲一遍代碼,在練習(xí)中發(fā)現(xiàn)問題,并解決,以后遇到同類型的問題也比較容易解決。
注意一些報錯的問題:
- 如果沒加 extern “C” 或者 沒將 C/C++ 文件配置到 CMake 文件上,可能會報
java.lang.UnsatisfiedLinkError: Native method not found: xxx錯誤 - 一般報
java.lang.NoSuchMethodError: no method with xxx錯誤,可能是因為 class 和方法不對應(yīng),env->GetObjectClass( jobject jobj)這里用錯了對象 - 報
java.lang.NoClassDefFoundError,可能是類名寫錯找不到類;
本文完整代碼可以到 GitHub 查看源碼: AndroidTrainingDemo/JNISample
參考資料:
專欄:JNI開發(fā)之旅 -貓的閣樓- CSDN博客
Andoid NDK編程1- 動態(tài)注冊native函數(shù) // Coding Life
Android Stuido Ndk-Jni 開發(fā):Jni中打印log信息 - 簡書