一、JNI基礎(chǔ)學(xué)習(xí)-JNI調(diào)用java原生方法
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sample_text.setOnClickListener {
callMethod("lilei", 18)
}
}
external fun callMethod(name: String, age: Int)
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
package com.microtechmd.jnidemo;
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 "Student{name='" + name + '\'' +", age=" + age +'}';
}
}
package com.microtechmd.jnidemo;
public class Person {
private void setStudent(Student student){
Log.d("dsh", "setStudent: "+student.toString());
}
public static String logcat(){
Log.d("dsh", "log : ");
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_microtechmd_jnidemo_MainActivity_callMethod(
JNIEnv *env,
jobject jo /* this */, jstring name, jint age) {
//創(chuàng)建Student對象
const char *student_class_str = "com/microtechmd/jnidemo/Student";
//獲取student class
jclass student_class = env->FindClass(student_class_str);
//根據(jù)class獲取student對象
jobject student_obj = env->AllocObject(student_class);
//獲取setName 方法iD
jmethodID setName_ID = env->GetMethodID(student_class, "setName", "(Ljava/lang/String;)V");
//執(zhí)行setName方法
env->CallVoidMethod(student_obj, setName_ID, name);
jmethodID setAge_ID = env->GetMethodID(student_class, "setAge", "(I)V");
env->CallVoidMethod(student_obj, setAge_ID, age);
const char *person_class_str = "com/microtechmd/jnidemo/Person";
jclass person_class = env->FindClass(person_class_str);
jobject person_object = env->AllocObject(person_class);
jmethodID setStudent_ID = env->GetMethodID(person_class, "setStudent",
("(Lcom/microtechmd/jnidemo/Student;)V"));
/執(zhí)行普通方法 需要對象和方法id、參數(shù)??偨Y(jié)類比java靜態(tài)方法和普通方法的調(diào)用
env->CallVoidMethod(person_object, setStudent_ID, student_obj);
//獲取靜態(tài)方法 ,不需要person對象
jmethodID log_ID = env->GetStaticMethodID(person_class, "logcat",
("()V"));
//執(zhí)行靜態(tài)方法
jstring string_obj = static_cast<jstring> ( env->CallStaticVoidMethod(person_class,log_ID));
const char *stringChar = env->GetStringUTFChars(string_obj,0);
env->ReleaseStringUTFChars(string_obj,stringChar); //回收對象
//JIN調(diào)用接口,有點(diǎn)類比Java發(fā)射
}
JNI調(diào)用java原生方法有四個重要的東西
一、class 類信息
二、method 方法信息
三、sign 方法簽名 ,里面包括了方法的參數(shù)類型信息 和返回信息,如(Ljava/lang/String;)V 代表的就是 void xxx(String)方法;其中構(gòu)造方法用 ,多個參數(shù)的方法這樣表示 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 代表 String xxxx(String , String )
四、實(shí)例對象
二、JNI基礎(chǔ)學(xué)習(xí)-String的處理
java傳給c一個string,javah生成了方法名后,
發(fā)現(xiàn)傳遞來的是一個jstring(因?yàn)樵赾里,是沒有string的),
public class Jni {
static {
System.loadLibrary("native-lib");
}
public native String study_string(String str);
}
//生成的頭文件
JNIEXPORT jstring JNICALL Java_jni_study_com_cvmars_Jni_study_1string
(JNIEnv *, jobject, jstring);
傳遞來的是一個jstring(因?yàn)樵赾里,是沒有string的), jstring其實(shí)是void*(任意類型)
我們需要調(diào)用一個方法,把jstring轉(zhuǎn)為C語言的char*類型,先看下這個工具方法:
#include <stdlib.h>
/**
* 把一個jstring轉(zhuǎn)換成一個c語言的char* 類型.
*/
char* _JString2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
rtn = (char*)malloc(alen+1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
實(shí)現(xiàn)native方法
JNIEXPORT jstring JNICALL Java_jni_study_com_jnibsetpractice_Jni_transe_1string
(JNIEnv *env, jobject instance, jstring jstr) {
//把一個jstring轉(zhuǎn)換成一個c語言的char* 類型
char *cStr = _JString2CStr(env, jstr);
//c語言拼接字符串
char *cNewStr = strcat(cStr, "簡單加密一下哈哈哈!!!");
// 把c語言里的char* 字符串轉(zhuǎn)成java認(rèn)識的字符串
return (*env)->NewStringUTF(env, cNewStr);
}
三、JNI基礎(chǔ)學(xué)習(xí)-在C里輸出log的辦法
- 在C 里輸入
- 在Android.mk里輸入
- 使用log
LOGD("length = %d", length);
四、JNI基礎(chǔ)學(xué)習(xí)- 數(shù)據(jù)類型和簽名機(jī)制
由于Java語言與C/C++語言數(shù)據(jù)類型的不匹配,需要單獨(dú)定義一系列的數(shù)據(jù)類型轉(zhuǎn)換關(guān)系來完成兩者之間的對等(或者說是映射)。下面給出jni與Java數(shù)據(jù)類型對應(yīng)表(jni類型均被定義在jni.h頭文件中),如下表1和表2,在jni函數(shù)中,需要使用以下jni類型來等價與Java語言對應(yīng)的類型。
1.基本類型對照表
<style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="Oe3MXsb6ys-1611024151055" data-version="1" data-is-embed="true"><colgroup><col width="249"><col width="258"><col width="270"></colgroup>
| Java類型 | JNI類型 | 描述 |
| boolean | Jboolean | 無符號8位 |
| byte | Jbyte | 無符號8位 |
| char | Jchar | 無符號16位 |
| short | Jshort | 有符號16位 |
| int | Jint | 有符號32位 |
| long | Jlong | 有符號64位 |
| float | Jfloat | 有符號32位 |
| double | Jdouble | 有符號64位 |</byte-sheet-html-origin>
2.引用類型對照表
<style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="qhTSRvgq9c-1611024151061" data-version="1" data-is-embed="true"><colgroup><col width="341"><col width="440"></colgroup>
| Java引用類型 | JNI類型 |
| boolean[] | jbooleanArray |
| byte[] | jbyteArray |
| char[] | jcharArray |
| short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| All objects | jobject |
| java.lang.Class | jclass |
| java.lang.String | jstring |
| Object[] | jobjectArray |
| java.lang.Throwable | jthrowable |</byte-sheet-html-origin>
1 深入理解JNIEnv
上面列出了JNI自定義類型,而為了操作這些類型,尤其是引用類型,就需要JNIEnv來協(xié)助完成。那么,什么是JNIEnv呢?實(shí)際上,JNIEnv的實(shí)體是一個名為JNINativeInterface的結(jié)構(gòu)體,而這個結(jié)構(gòu)體又是什么呢?JNINativeInterface結(jié)構(gòu)體定義在頭文件jni.h中,是一個復(fù)雜的函數(shù)指針集合,每一個函數(shù)指針又會指向一個本地實(shí)現(xiàn)函數(shù),來完成特定的功能。諸如常見的New StringUTF,F(xiàn)indClass都定義在其中,如下列出了部分內(nèi)容:
/* jni.h */#if defined(__cplusplus)typedef _JNIEnv JNIEnv; // C++typedef _JavaVM JavaVM;#elsetypedef const struct JNINativeInterface* JNIEnv; // Ctypedef const struct JNIInvokeInterface* JavaVM;#endifstruct JNINativeInterface {
…
jclass (*FindClass)(JNIEnv*, const char*);
…
jstring (*NewString)(JNIEnv*, const jchar*, jsize);
…
void (*SetCharArrayRegion)(JNIEnv*, jcharArray,
jsize, jsize, const jchar*);
…
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,
jint);
…
jint (*GetJavaVM)(JNIEnv*, JavaVM**);
…./* added in JNI 1.6 */// … 表示省略了部分內(nèi)容
};
下圖來幫助理解這個復(fù)雜的指向關(guān)系:
有了JNIEnv*指針,就可以使用函數(shù)指針調(diào)用特定的實(shí)現(xiàn)函數(shù),來完成特定需求的功能。需要注意的是,env變量是線程線程相關(guān)的,不可從一個線程傳遞env變量到另外一個線程。
那么又是如何使線程獲得這個JNIEnv結(jié)構(gòu)體指針的呢?這里涉及到一個重要的函數(shù)JNI_OnLoad(JavaVM* vm,void* reserved),當(dāng)通過System. loadLibrary()方法來加載我們指定的動態(tài)庫(如.so庫)時,Java虛擬機(jī)會檢測庫中是否實(shí)現(xiàn)了JNI_OnLoad函數(shù),如果實(shí)現(xiàn)了則這個函數(shù)就會被調(diào)用,并且一個代表JVM的對象vm被作為參數(shù)傳遞進(jìn)來,這個對象一個進(jìn)程只有一份,可以通過它的AttachCurrentThread方法來獲得JNIEnv*對象,當(dāng)我們的線程完成特定任務(wù)退出之前,應(yīng)該調(diào)用vm的DetachCurrentThread來釋放資源。
上述方法均被定義在jni.h,如下:
/* jni.h */#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;#endif/*
* JNI invocation interface.
*/struct JNIInvokeInterface { // C// ....
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
/*
* C++ version.
*/struct _JavaVM { // C++const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }#endif /*__cplusplus*/
};
/*
* Prototypes for functions exported by loadable shared libs. These are
* called by JNI, not provided by JNI.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);// ....
在JNI_OnLoad()函數(shù)中,也可以通過vm->GetEnv((void*)&env來獲得JNIEnv指針。JNI_OnLoad()函數(shù)基本功能是確定并返回Java虛擬機(jī)支持的JNI版本,我們還可以用作其他用途(諸如做一些初始化工作),一個重要的用途是實(shí)現(xiàn)JNI函數(shù)的動態(tài)注冊。
與JNI_OnLoad()函數(shù)正好相反,當(dāng)共享庫被卸載時,會調(diào)用JNI_OnUnload()函數(shù),我們可以做一些收尾的工作。
2 JNI函數(shù)的注冊過程
在前面講解了JNI函數(shù),并沒有深入探究Java層函數(shù)與jni函數(shù)的對應(yīng)關(guān)系的建立,那么這種關(guān)聯(lián)是怎樣建立的,或者說當(dāng)發(fā)起java native方法的調(diào)用時,是如何找到與之對應(yīng)的jni函數(shù)的呢?這個過程可以分別用靜態(tài)注冊和動態(tài)注冊的方式來完成。其實(shí)前面已經(jīng)講過了靜態(tài)注冊的原理。沒錯!就是命名規(guī)范,按照前面說的來命名jni函數(shù),就可以實(shí)現(xiàn),這里就不再贅述了。接下來,介紹JNI函數(shù)的動態(tài)注冊過程。
何為動態(tài)注冊呢?說的直白點(diǎn)就是手動的參與它的注冊過程,讓JNI函數(shù)在一加載完.so動態(tài)庫后就完成它的注冊過程(使之與對應(yīng)java native函數(shù)關(guān)聯(lián)起來),而不是等到調(diào)用時再來進(jìn)行注冊,以提高調(diào)用效率,并且我們也不用遵守前面的命名規(guī)范了,可以給jni函數(shù)取自己認(rèn)為合適的名字。
要完成這個動態(tài)注冊過程,就需要使用在上面提到過的JNI_OnLoad函數(shù),它是在.so動態(tài)庫加載后就會被調(diào)用的,而這又早于JNI函數(shù)的調(diào)用時機(jī),因此在這個函數(shù)里實(shí)現(xiàn)注冊過程是很合理的。
要完成動態(tài)注冊,方法一可以選擇使用AndroidRuntime類的registerNativeMethods方法來完成注冊,這個方法原型如下:
/*
* Register native methods using JNI.
*/
static int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
使用這個函數(shù)需要提供包含進(jìn)行注冊的jni函數(shù)的類的全路徑(如項(xiàng)目中的OkayOps 類,全路徑為com/yu/ops/OkayOps),要進(jìn)行注冊的方法信息結(jié)構(gòu)體數(shù)組(JNINativeMethod)及方法個數(shù)。JNINativeMethod是一個C結(jié)構(gòu)體,用于存儲Java native方法與JNI函數(shù)的一一對應(yīng)關(guān)系,包含的信息有native方法名、函數(shù)簽名、函數(shù)指針。它的定義如下所示:
typedef struct {
const char* name; // Java層聲明的native函數(shù)的名字,不需要帶路徑 。
const char* signature; // Java層聲明的native函數(shù)簽名信息,用字符串表示
void* fnPtr; //JNI 層對應(yīng)函數(shù)的函數(shù)指針,它的類型void*
} JNINativeMethod;
上面涉及一個新概念函數(shù)簽名,現(xiàn)在只需知道它是用來標(biāo)識匹配哪個java的native方法即可,為了分析注冊過程的條理清晰,將在下一節(jié)詳細(xì)介紹。在registerNativeMethods方法的最后又調(diào)用了jniRegisterNativeMethods方法來完成注冊,這個函數(shù)是在JNIHelp.h中聲明(Android提供的幫助類來方便使用jni,路徑android/libnativehelper/include/nativehelper/JNIHelp.h,實(shí)現(xiàn)在JNIHelp.cpp),可以先來看看這個方法:
/* JNIHelp.cpp */
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
ALOGV("Registering %s's %d native methods...", className, numMethods);
// 獲取指定類名的Class對象,并存儲在局部引用中
scoped_local_ref<jclass> c(env, findClass(env, className));
if (c.get() == NULL) { // 獲取class對象為NULL
char* tmp;
const char* msg;
if (asprintf(&tmp,
"Native registration unable to find class '%s'; aborting...",
className) == -1) {
// Allocation failed, print default warning.
msg = "Native registration unable to find class; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
// 調(diào)用JNIEnv的RegisterNatives來完成注冊
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* tmp;
const char* msg;
if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
// Allocation failed, print default warning.
msg = "RegisterNatives failed; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
return 0;
}
可以發(fā)現(xiàn),jniRegisterNativeMethods函數(shù)并不是具體實(shí)現(xiàn),最終它會調(diào)用JNIEnv的RegisterNatives函數(shù)來完成JNI函數(shù)的注冊。到此注冊過程分析完成,終究是回到JNIEnv上。下面看看RegisterNatives函數(shù)原型:
jint (*RegisterNatives) (JNIEnv* env, jclass clazz, const JNINativeMethod* gMethods , jint numMethods);
可以發(fā)現(xiàn),它和AndroidRuntime::registerNativeMethods函數(shù)的參數(shù)較為類似,除了第二個參數(shù)不同以外,其他均相同。而第二個參數(shù)正是要進(jìn)行動態(tài)注冊的類的Class運(yùn)行時類,可以使用JNIEnv的FindClass函數(shù)來獲取。
第二種進(jìn)行動態(tài)注冊的方式就是基于上面的分析,即:第一步,使用JNIEnv的FindClass函數(shù)來拿到需要進(jìn)行動態(tài)注冊的類的運(yùn)行時Class類;第二步,直接使用JNIEnv的RegisterNatives函數(shù)來完成JNI函數(shù)的注冊。
到此,我們分析了兩種方案來完成JNI函數(shù)動態(tài)注冊的目標(biāo)。第一種,分析了使用AndroidRuntime::registerNativeMethods函數(shù)來完成動態(tài)注冊的流程,使用該函數(shù)總體上來說使用方便,但流程較為復(fù)雜,第二種,使用JNIEnv的RegisterNatives函數(shù)完成動態(tài)注冊,這種方法流程簡單,但需要自個獲取運(yùn)行時Class類,稍顯得煩瑣點(diǎn)。
本文實(shí)現(xiàn)注冊的代碼如下:
// 需要注冊的方法信息表
static JNINativeMethod method_table[] = {
{"NativeReadOkayData", "([B)I", (void*)Java_android_com_read_yu_data},
{"NativeWriteOkayData", "([BI)I", (void*) Java_android_com_write_yu_data},
};
// 包含本地方法的類的全路徑
static const char* classPathName="com/yu/ops/OkayOps";
// 使用AndroidRuntime的registerNativeMethods方法來完成注冊
static int register_com_yu_signature_ops(JNIEnv *env)
{
LOGI("register_com_yu_ops_OkayOps");
return AndroidRuntime::registerNativeMethods(env,classPathName,method_table,NELEM(method_table));
}
// 加載動態(tài)庫的時候被回調(diào)
jint JNI_OnLoad(JavaVM* vm,void* reserved)
{
LOGI("JNI_OnLoad");
JNIEnv* env = NULL;
jint result = -1;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6) != JNI_OK)
{
goto bail;
}
LOGI("register method");
if(register_com_yu_signature_ops(env) < 0) // 注冊
{
goto bail;
}
init(); // 做一些初始化工作
return JNI_VERSION_1_6;
bail:
return result;
}
3 簽名機(jī)制
在上面動態(tài)注冊小節(jié)提到一個函數(shù)簽名(signature)的概念,這是用來干什么的呢?了解java語言的都知道它有一種方法重載機(jī)制,因此,為了能夠調(diào)用正確的java層native方法,光憑方法名稱是不夠的,還需要知道它的具體參數(shù)與返回值。函數(shù)簽名就是函數(shù)的參數(shù)與返回值的結(jié)合體,用來進(jìn)行精準(zhǔn)匹配。
函數(shù)簽名由字符串組成,第一部分是包含在圓括號()里的,用來說明參數(shù)類型,第二部分則跟的是返回值類型。比如”([Ljava/lang/Object;)Z”就是參數(shù)為Object[],返回值是boolean的函數(shù)的簽名。下表列出類型與簽名標(biāo)識的對應(yīng)關(guān)系:
| Java類型 | 類型標(biāo)識 |
| boolean | Z |
| byte | B |
| char | C |
| short | S |
| int | I |
| long | J |
| float | F |
| double | D |
| String | L/java/lang/String; |
| int[] | [I |
| Object[] | [L/java/lang/Object; |
int[]的標(biāo)識是[I,其他基本數(shù)據(jù)類型的標(biāo)識基本類似,用[+類型標(biāo)識組合。需要注意的是,除了基本數(shù)據(jù)類型的數(shù)組以外,引用類型的標(biāo)識后都需要跟上一個分號。一般,人為的寫簽名字符串難免會出錯,而且類型簽名標(biāo)識又難以記憶,所幸的是java提供了相關(guān)命令來快速生成簽名信息。到要生成簽名的項(xiàng)目的bin目錄下,使用javap命令加 –s選項(xiàng)來快速生成簽名信息,如下:
D:\code\yu_jar\bin>javap -s com.yu.ops.OkayOps
Compiled from "OkayOps.java"
public class com.yu.ops.OkayOps {
public java.lang.String SERVICE;
descriptor: Ljava/lang/String;
static {};
descriptor: ()V
public com.yu.ops.OkayOps();
descriptor: ()V
public final int yu_read(byte[]);
descriptor: ([B)I
public final void yu_write(byte[]);
descriptor: ([B)V
public native int NativeReadOkayData(byte[]);
descriptor: ([B)I
public native int NativeWriteOkayData(byte[], int);
descriptor: ([BI)I
}
在方法下面的descriptor的內(nèi)容即是所需要的簽名信息。簽名信息比較有用,在JNI函數(shù)的調(diào)用中,經(jīng)常會需要以簽名作為參數(shù)。
在jni.h頭文件我們可以看到基本類型方法簽名定義,如下:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;