注:原文地址html
緊接上篇:Android NDK開發:JNI基礎篇 | cfanr,這篇主要介紹 JNI Native 層調用Java 層的代碼(涉及JNI 數據類型映射和描述符的使用)和如何動態註冊 JNI。java
在開始實戰練習前,你須要先大體瞭解運行一個 Hello World 的項目大概須要作什麼,有哪些配置以及配置的具體意思。 Android Studio(2.2以上版本)提供兩種方式編譯原生庫:CMake( 默認方式) 和 ndk-build。對於初學者能夠先了解 CMake 的方式,另外,對於本文能夠暫時不用瞭解 so 庫如何編譯和使用。android
一個 Hello World 的 NDK 項目很簡單,按照流程新建一個 native 庫工程就能夠,因爲太簡單,並且網上也有不少教程,這裏就不必浪費時間再用圖文介紹了。詳細操做方法,能夠參考這篇文章,AS2.2使用CMake方式進行JNI/NDK開發-於連林- CSDN博客c++
列出項目中涉及 NDK 的內容或配置幾點須要注意的地方:git
# 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.生成函數庫的名字,須要寫到程序中的 so 庫的名字 native-lib # Sets the library as a shared library. 生成動態函數 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. 設置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. 目標庫, 和上面生成的函數庫名字一致 native-lib # Links the target library to the log library # included in the NDK. 鏈接的庫,根據log-lib變量對應liblog.so函數庫 ${log-lib} )
build.gradle 文件,注意兩個 externalNativeBuild {}
的配置github
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 標準,則改成 "-std=c++11" // 生成.so庫的目標平臺,使用的是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 調用 so 庫數組
public class MainActivity extends AppCompatActivity { // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } //…… }
另外,還須要回顧上篇 JNI 基礎篇-靜態註冊也提到 JNI 的函數命名規則:JNIEXPORT 返回值 JNICALL Java_全路徑類名_方法名_參數簽名(JNIEnv* , jclass, 其它參數);
其中第二個參數,當 java native 方法是 static 時,爲 jclass,當爲非靜態方法時,爲 jobject,爲了簡單起見,下面的例子 JNI 函數都標記extern "C"
,函數名就不須要寫參數簽名了數據結構
注:如下練習中 Java native 方法都是非靜態的
步驟:多線程
env->GetObjectClass(jobject)
獲取Java 對象的 class 類,返回一個 jclass;env->GetFieldID(jclazz, fieldName, signature)
獲得該實例域(變量)的 id,即 jfieldID;若是變量是靜態 static 的,則調用的方法爲 GetStaticFieldID
env->Get{type}Field(jobject, fieldId)
獲得該變量的值。其中{type} 是變量的類型;若是變量是靜態 static 的,則調用的方法是GetStatic{type}Field(jclass, fieldId)
,注意 static 的話, 是使用 jclass 做爲參數;native方法定義和調用
public int num = 10; public native int addNum(); FieldJni fieldJni = new FieldJni(); Log.e(TAG, "調用前:num = " + fieldJni.num); Log.e(TAG, "調用後:" + fieldJni.addNum());
C++層:
extern "C" JNIEXPORT jint JNICALL Java_cn_cfanr_jnisample_FieldJni_addNum(JNIEnv *env, jobject jobj) { //獲取實例對應的 class jclass jclazz = env->GetObjectClass(jobj); //經過class獲取相應的變量的 field id jfieldID fid = env->GetFieldID(jclazz, "num", "I"); //經過 field id 獲取對應的值 jint num = env->GetIntField(jobj, fid); //注意,不是用 jclazz, 使用 jobj num++; return num; }
輸出結果:
MainActivity: 調用前:num = 10 MainActivity: 調用後:11
因爲 jclass 也是繼承 jobject,因此使用 GetIntField 時不要混淆兩個參數
native方法定義和調用
public static String name = "cfanr"; public native void accessStaticField(); //調用 Log.e(TAG, "調用前:name = " + fieldJni.name); fieldJni.accessStaticField(); Log.e(TAG, "調用後:" + 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類型的變量,設置到java env->SetStaticObjectField(jclazz, fid, new_str); }
輸出結果:
MainActivity: 調用前:name = cfanr MainActivity: 調用後:hello, cfanr
須要注意的是,獲取 java 靜態變量,都是調用 JNI 相應靜態的函數,不能調用非靜態的,同時留意傳入的參數是 jclass,而不是 jobject
native方法定義和調用
private int age = 21; public native void accessPrivateField(); public int getAge() { return age; } //調用 Log.e(TAG, "調用前:age = " + fieldJni.getAge()); fieldJni.accessPrivateField(); Log.e(TAG, "調用後: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); }
輸出結果:
MainActivity: 調用前:age = 21 MainActivity: 調用後:age = 18
步驟:(和訪問 Java 對象的變量有點類型)
env->GetObjectClass(jobject)
獲取Java 對象的 class 類,返回一個 jclass;env->GetMethodID(jclass, methodName, sign)
獲取到 Java 對象的方法 Id,即 jmethodID,當獲取的方法是 static 的時,使用GetStaticMethodID
;env->Call{type}Method(jobject, jmethod, param...)
實現調用 Java的方法;若調用的是 static 方法,則使用CallStatic{type}Method(jclass, jmethod, param...)
,使用的是 jclassnative方法定義和調用
private String sex = "female"; public void setSex(String sex) { this.sex = sex; } public String getSex(){ return sex; } public native void accessPublicMethod(); //調用 MethodJni methodJni = new MethodJni(); Log.e(TAG, "調用前:getSex() = " + methodJni.getSex()); methodJni.accessPublicMethod(); Log.e(TAG, "調用後:getSex() = " + methodJni.getSex());
C++
extern "C" JNIEXPORT void JNICALL Java_cn_cfanr_jnisample_MethodJni_accessPublicMethod(JNIEnv *env, jobject jobj) { //1.獲取對應 class 的實體類 jclass jclazz = env->GetObjectClass(jobj); //2.獲取方法的 id jmethodID mid = env->GetMethodID(jclazz, "setSex", "(Ljava/lang/String;)V"); //3.字符數組轉換爲字符串 char c[10] = "male"; jstring jsex = env->NewStringUTF(c); //4.經過該 class 調用對應的 public 方法 env->CallVoidMethod(jobj, mid, jsex); }
結果:
MainActivity: 調用前:getSex() = female MainActivity: 調用後:getSex() = male
調用 java private 方法也是相似, Java 的訪問域修飾符對 C++無效
native方法定義和調用
private static int height = 170; public static int getHeight() { return height; } public native int accessStaticMethod(); //調用 Log.e(TAG, "調用靜態方法:getHeight() = " + methodJni.accessStaticMethod());
C++
extern "C" JNIEXPORT jint JNICALL Java_cn_cfanr_jnisample_MethodJni_accessStaticMethod(JNIEnv *env, jobject jobj) { //1.獲取對應 class 實體類 jclass jclazz = env->GetObjectClass(jobj); //2.經過 class 類找到對應的方法 id jmethodID mid = env->GetStaticMethodID(jclazz, "getHeight", "()I"); //注意靜態方法是調用GetStaticMethodID, 不是GetMethodID //3.經過 class 調用對應的靜態方法 return env->CallStaticIntMethod(jclazz, mid); }
輸出結果:
MainActivity: 調用靜態方法:getHeight() = 170
注意調用的靜態方法要一致。
native方法定義和調用
public class SuperJni { public String hello(String name) { return "welcome to JNI world, " + name; } } public class MethodJni extends SuperJni{ public native String accessSuperMethod(); } //調用 Log.e(TAG, "調用父類方法: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 找到對應的方法 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); }
注意兩點不一樣的地方,
native 方法既能夠傳遞基本類型參數給 JNI(能夠不通過轉換直接使用),也能夠傳遞複雜的類型(須要轉換爲 C/C++ 的數據結構才能使用),如數組,String 或自定義的類等。
基礎類型,這裏就不舉例子了,詳細能夠看 GitHub 上的源碼: AndroidTrainingDemo/JNISample
要用到的 JNI 函數:
GetArrayLength(j{type}Array)
,type 爲基礎類型;Get{type}ArrayElements(jarr, 0)
env->GetMethodID(jclass, methodName, sign)
獲取,方法 name 是<init>;env->NewObject(jclass, constructorMethodID, param...)
,無參構造函數 param 則爲空計算整型數組參數的和
native方法定義和調用
public native int intArrayMethod(int[] arr); //調用 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循環中 jint i=0; for(; i < len; i++) { sum += arr[i]; } env->ReleaseIntArrayElements(arr_, arr, 0); //釋放內存 return sum; }
輸出結果:
MainActivity: intArrayMethod: 39
Person 定義,native方法定義和調用
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+"}"; } } //傳遞複雜對象person,再jni函數中新構造一個person傳回java層輸出 public native Person objectMethod(Person person); //調用 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); }
輸出結果
MainActivity: objectMethod: Person :{ name: cfanr, age: 21}
注意:傳遞對象時,獲取的 jclass 是獲取該參數對象的 jobject 獲取,而不是第二個參數(定義該 native 方法的對象)取;
native方法定義和調用
public native ArrayList<Person> personArrayListMethod(ArrayList<Person> persons); //調用 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, "調用前:java list = " + personList.toString()); Log.e(TAG, "調用後:jni list = " + paramsJni.personArrayListMethod(personList).toString());
C++
extern "C" JNIEXPORT jobject JNICALL Java_cn_cfanr_jnisample_ParamsJni_personArrayListMethod(JNIEnv *env, jobject jobj, jobject persons) { //經過參數獲取 ArrayList 對象的 class jclass clazz = env->GetObjectClass(persons); if(clazz == NULL) { return env->NewStringUTF("not find class"); } //獲取 ArrayList 無參數的構造函數 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 的構造函數的 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; }
輸出結果:
MainActivity: 調用前:java list = [Person :{ name: cfanr, age: 10}, Person :{ name: cfanr, age: 11}, Person :{ name: cfanr, age: 12}] MainActivity: 調用後:jni list = [Person :{ name: Native, age: 18}, Person :{ name: Native, age: 19}, Person :{ name: Native, age: 20}]
複雜的集合參數也是須要經過獲取集合的 class 和對應的方法來調用實現的
其中 isCopy 是取值爲JNI_TRUE和JNI_FALSE(或者1,0),值爲JNI_TRUE,表示返回JVM內部源字符串的一份拷貝,併爲新產生的字符串分配內存空間。若是值爲JNI_FALSE,表示返回JVM內部源字符串的指針,意味着能夠經過指針修改源字符串的內容,不推薦這麼作,由於這樣作就打破了Java字符串不能修改的規定;Java默認使用Unicode編碼,而C/C++默認使用UTF編碼,因此在本地代碼中操做字符串的時候,必須使用合適的JNI函數把jstring轉換成C風格的字符串
- UTF-8字符:const char* GetStringUTFChars(jstring string, jboolean* isCopy)
- Unicode字符:const jchar* GetStringChars(jstring string, jboolean* isCopy)
void ReleaseStringUTFChars(jstring string, const char* utf)
void ReleaseStringChars(jstring string, const jchar* chars)
代碼示例就不寫了,其餘詳細可參考:
JNI開發之旅(9)JNI函數字符串處理 - 貓的閣樓 - CSDN博客
JNI/NDK開發指南(四)——字符串處理 - 技術改變生活- CSDN博客
學了上面的練習,發現靜態註冊的方式仍是挺麻煩的,生成的 JNI 函數名太長,文件、類名、變量或方法重構時,須要從新修改頭文件或 C/C++ 內容代碼(並且仍是各個函數都要修改,沒有一個統一的地方),動態註冊 JNI 的方法就能夠解決這個問題。
由上篇回顧下,Android NDK開發:JNI基礎篇 | cfanr
動態註冊 JNI 的原理:直接告訴 native 方法其在JNI 中對應函數的指針。經過使用 JNINativeMethod 結構來保存 Java native 方法和 JNI 函數關聯關係,步驟:
registerNatives(JNIEnv* env)
註冊類的全部本地方法;代碼實例:
native 方法和調用:
public class DynamicRegisterJni { public native String getStringFromCpp(); } //調用 String hello = new DynamicRegisterJni().getStringFromCpp(); Log.e(TAG, hello);
C++動態註冊 JNI 代碼:
#include <jni.h> #include "android/log.h" #include <stdio.h> #include <string> #ifndef LOG_TAG #define LOG_TAG "JNI_LOG" //Log 的 tag 名字 //定義各類類型 Log 的函數別名 #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"; //定義對應Java native方法的 C++ 函數,函數名能夠隨意命名 static jstring sayHello(JNIEnv *env, jobject) { LOGI("hello, this is native log."); const char* hello = "Hello from C++."; return env->NewStringUTF(hello); } /* * 定義函數映射表(是一個數組,能夠同時定義多個函數的映射) * 參數1:Java 方法名 * 參數2:方法描述符,也就是簽名 * 參數3:C++定義對應 Java native方法的函數名 */ static JNINativeMethod jni_Methods_table[] = { {"getStringFromCpp", "()Ljava/lang/String;", (void *) sayHello}, }; //根據函數映射表註冊函數 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
輸出結果:
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 調用 Android 的 Log,只須要引入#include "android/log.h"
頭文件和對函數別名命名便可。其餘具體說明見上面代碼。
實際開發中能夠採起動態和靜態註冊結合的方式,寫一個Java 的 native 方法完成調用動態註冊的代碼,大概代碼以下:
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)); }
雖然都是按照網上的例子作的練習記錄,但仍是遇到很多小問題的,不過只要仔細查找,也比較容易發現問題的所在,之前以爲 JNI 挺難懂的,但此次練習下來,以爲 JNI 也只不過是一套語法規則而已,按照規則去實現代碼也不算特別難,固然這只是 JNI 的一小部份內容,JNI 還有不少內容,如反射、異常處理、多線程、NIO 等。雖然此次練習比較簡單,但建議仍是本身親自敲一遍代碼,在練習中發現問題,並解決,之後遇到同類型的問題也比較容易解決。
注意一些報錯的問題:
java.lang.UnsatisfiedLinkError: Native method not found: xxx
錯誤java.lang.NoSuchMethodError: no method with xxx
錯誤,多是由於 class 和方法不對應,env->GetObjectClass( jobject jobj)
這裏用錯了對象java.lang.NoClassDefFoundError
,多是類名寫錯找不到類;本文完整代碼能夠到 GitHub 查看源碼: AndroidTrainingDemo/JNISample
參考資料:
專欄:JNI開發之旅 -貓的閣樓- CSDN博客
Andoid NDK編程1- 動態註冊native函數 // Coding Life
Android Stuido Ndk-Jni 開發:Jni中打印log信息 - 簡書
做者:cfanr 連接:https://www.jianshu.com/p/464cd879eaba 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。