一、JNI基础学习-JNI调用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) {
//创建Student对象
const char *student_class_str = "com/microtechmd/jnidemo/Student";
//获取student class
jclass student_class = env->FindClass(student_class_str);
//根据class获取student对象
jobject student_obj = env->AllocObject(student_class);
//获取setName 方法iD
jmethodID setName_ID = env->GetMethodID(student_class, "setName", "(Ljava/lang/String;)V");
//执行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"));
/执行普通方法 需要对象和方法id、参数。总结类比java静态方法和普通方法的调用
env->CallVoidMethod(person_object, setStudent_ID, student_obj);
//获取静态方法 ,不需要person对象
jmethodID log_ID = env->GetStaticMethodID(person_class, "logcat",
("()V"));
//执行静态方法
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调用接口,有点类比Java发射
}
JNI调用java原生方法有四个重要的东西
一、class 类信息
二、method 方法信息
三、sign 方法签名 ,里面包括了方法的参数类型信息 和返回信息,如(Ljava/lang/String;)V 代表的就是 void xxx(String)方法;其中构造方法用 ,多个参数的方法这样表示 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 代表 String xxxx(String , String )
四、实例对象
二、JNI基础学习-String的处理
java传给c一个string,javah生成了方法名后,
发现传递来的是一个jstring(因为在c里,是没有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(因为在c里,是没有string的), jstring其实是void*(任意类型)
我们需要调用一个方法,把jstring转为C语言的char*类型,先看下这个工具方法:
#include <stdlib.h>
/**
* 把一个jstring转换成一个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;
}
实现native方法
JNIEXPORT jstring JNICALL Java_jni_study_com_jnibsetpractice_Jni_transe_1string
(JNIEnv *env, jobject instance, jstring jstr) {
//把一个jstring转换成一个c语言的char* 类型
char *cStr = _JString2CStr(env, jstr);
//c语言拼接字符串
char *cNewStr = strcat(cStr, "简单加密一下哈哈哈!!!");
// 把c语言里的char* 字符串转成java认识的字符串
return (*env)->NewStringUTF(env, cNewStr);
}
三、JNI基础学习-在C里输出log的办法
- 在C 里输入
- 在Android.mk里输入
- 使用log
LOGD("length = %d", length);
四、JNI基础学习- 数据类型和签名机制
由于Java语言与C/C++语言数据类型的不匹配,需要单独定义一系列的数据类型转换关系来完成两者之间的对等(或者说是映射)。下面给出jni与Java数据类型对应表(jni类型均被定义在jni.h头文件中),如下表1和表2,在jni函数中,需要使用以下jni类型来等价与Java语言对应的类型。
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来协助完成。那么,什么是JNIEnv呢?实际上,JNIEnv的实体是一个名为JNINativeInterface的结构体,而这个结构体又是什么呢?JNINativeInterface结构体定义在头文件jni.h中,是一个复杂的函数指针集合,每一个函数指针又会指向一个本地实现函数,来完成特定的功能。诸如常见的New StringUTF,FindClass都定义在其中,如下列出了部分内容:
/* 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 */// … 表示省略了部分内容
};
下图来帮助理解这个复杂的指向关系:
有了JNIEnv*指针,就可以使用函数指针调用特定的实现函数,来完成特定需求的功能。需要注意的是,env变量是线程线程相关的,不可从一个线程传递env变量到另外一个线程。
那么又是如何使线程获得这个JNIEnv结构体指针的呢?这里涉及到一个重要的函数JNI_OnLoad(JavaVM* vm,void* reserved),当通过System. loadLibrary()方法来加载我们指定的动态库(如.so库)时,Java虚拟机会检测库中是否实现了JNI_OnLoad函数,如果实现了则这个函数就会被调用,并且一个代表JVM的对象vm被作为参数传递进来,这个对象一个进程只有一份,可以通过它的AttachCurrentThread方法来获得JNIEnv*对象,当我们的线程完成特定任务退出之前,应该调用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()函数中,也可以通过vm->GetEnv((void*)&env来获得JNIEnv指针。JNI_OnLoad()函数基本功能是确定并返回Java虚拟机支持的JNI版本,我们还可以用作其他用途(诸如做一些初始化工作),一个重要的用途是实现JNI函数的动态注册。
与JNI_OnLoad()函数正好相反,当共享库被卸载时,会调用JNI_OnUnload()函数,我们可以做一些收尾的工作。
2 JNI函数的注册过程
在前面讲解了JNI函数,并没有深入探究Java层函数与jni函数的对应关系的建立,那么这种关联是怎样建立的,或者说当发起java native方法的调用时,是如何找到与之对应的jni函数的呢?这个过程可以分别用静态注册和动态注册的方式来完成。其实前面已经讲过了静态注册的原理。没错!就是命名规范,按照前面说的来命名jni函数,就可以实现,这里就不再赘述了。接下来,介绍JNI函数的动态注册过程。
何为动态注册呢?说的直白点就是手动的参与它的注册过程,让JNI函数在一加载完.so动态库后就完成它的注册过程(使之与对应java native函数关联起来),而不是等到调用时再来进行注册,以提高调用效率,并且我们也不用遵守前面的命名规范了,可以给jni函数取自己认为合适的名字。
要完成这个动态注册过程,就需要使用在上面提到过的JNI_OnLoad函数,它是在.so动态库加载后就会被调用的,而这又早于JNI函数的调用时机,因此在这个函数里实现注册过程是很合理的。
要完成动态注册,方法一可以选择使用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);
}
使用这个函数需要提供包含进行注册的jni函数的类的全路径(如项目中的OkayOps 类,全路径为com/yu/ops/OkayOps),要进行注册的方法信息结构体数组(JNINativeMethod)及方法个数。JNINativeMethod是一个C结构体,用于存储Java native方法与JNI函数的一一对应关系,包含的信息有native方法名、函数签名、函数指针。它的定义如下所示:
typedef struct {
const char* name; // Java层声明的native函数的名字,不需要带路径 。
const char* signature; // Java层声明的native函数签名信息,用字符串表示
void* fnPtr; //JNI 层对应函数的函数指针,它的类型void*
} JNINativeMethod;
上面涉及一个新概念函数签名,现在只需知道它是用来标识匹配哪个java的native方法即可,为了分析注册过程的条理清晰,将在下一节详细介绍。在registerNativeMethods方法的最后又调用了jniRegisterNativeMethods方法来完成注册,这个函数是在JNIHelp.h中声明(Android提供的帮助类来方便使用jni,路径android/libnativehelper/include/nativehelper/JNIHelp.h,实现在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);
}
// 调用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;
}
可以发现,jniRegisterNativeMethods函数并不是具体实现,最终它会调用JNIEnv的RegisterNatives函数来完成JNI函数的注册。到此注册过程分析完成,终究是回到JNIEnv上。下面看看RegisterNatives函数原型:
jint (*RegisterNatives) (JNIEnv* env, jclass clazz, const JNINativeMethod* gMethods , jint numMethods);
可以发现,它和AndroidRuntime::registerNativeMethods函数的参数较为类似,除了第二个参数不同以外,其他均相同。而第二个参数正是要进行动态注册的类的Class运行时类,可以使用JNIEnv的FindClass函数来获取。
第二种进行动态注册的方式就是基于上面的分析,即:第一步,使用JNIEnv的FindClass函数来拿到需要进行动态注册的类的运行时Class类;第二步,直接使用JNIEnv的RegisterNatives函数来完成JNI函数的注册。
到此,我们分析了两种方案来完成JNI函数动态注册的目标。第一种,分析了使用AndroidRuntime::registerNativeMethods函数来完成动态注册的流程,使用该函数总体上来说使用方便,但流程较为复杂,第二种,使用JNIEnv的RegisterNatives函数完成动态注册,这种方法流程简单,但需要自个获取运行时Class类,稍显得烦琐点。
本文实现注册的代码如下:
// 需要注册的方法信息表
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));
}
// 加载动态库的时候被回调
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 签名机制
在上面动态注册小节提到一个函数签名(signature)的概念,这是用来干什么的呢?了解java语言的都知道它有一种方法重载机制,因此,为了能够调用正确的java层native方法,光凭方法名称是不够的,还需要知道它的具体参数与返回值。函数签名就是函数的参数与返回值的结合体,用来进行精准匹配。
函数签名由字符串组成,第一部分是包含在圆括号()里的,用来说明参数类型,第二部分则跟的是返回值类型。比如”([Ljava/lang/Object;)Z”就是参数为Object[],返回值是boolean的函数的签名。下表列出类型与签名标识的对应关系:
| Java类型 | 类型标识 |
| 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[]的标识是[I,其他基本数据类型的标识基本类似,用[+类型标识组合。需要注意的是,除了基本数据类型的数组以外,引用类型的标识后都需要跟上一个分号。一般,人为的写签名字符串难免会出错,而且类型签名标识又难以记忆,所幸的是java提供了相关命令来快速生成签名信息。到要生成签名的项目的bin目录下,使用javap命令加 –s选项来快速生成签名信息,如下:
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的内容即是所需要的签名信息。签名信息比较有用,在JNI函数的调用中,经常会需要以签名作为参数。
在jni.h头文件我们可以看到基本类型方法签名定义,如下:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;