开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
JNI系列文章导引
JNI指南
介绍JNI的编写规则,方法注册以及数据类型。
题外话--我为什么写这个系列
JNI,在我刚入门Android的那时候,基本也就只是听说过,那时候都说JNI,那是中高级Android开发大佬们才能熟练运用的技能。从那时开始,JNI在我心里就是高级技术的象征。当然,那只是对于小白的我这么认为的,以至于工作几年后每每有迫不得已要用到时,都是到处胡乱查一通,然后放弃,交给其他同事处理,未免书到用时方恨少的感慨。
由于这个认知,每次想要学习这门”高深莫测“的技能时,都想着我一定要找一本系统学习的书,或者一门系统介绍的课程,从入门到精通。但是每次饱含热情去搜索,去google,最后发现,都是零散的几篇文章就介绍完毕了,就没了,没了....以至于我一直不敢下定决心掌握这门技术,以为自己认知到的只是片面,望而生畏。
工作几年后,对Android有了更深入的了解,在项目中看到同事的C代码,似乎也能从容淡定看待,也决心学习C,C++,掌握JNI这门技术。慢慢接触后发现,JNI真的也就如在网上看到的那般,内容可能并没有那么多,也并没有那么深奥难懂。
所以,我想写下这个系列,虽然也不长,有些也是借鉴了前人的经验,但是我想给看到的同学,想要了解JNI的同学打打气,JNI并不需要远远仰望,想学习掌握,随时都可以,不要怕,加油。
内容有不全,有疑问的地方,还请评论区指正,也欢迎一起探讨。
Hello JNI
编写JNI
,Java
来调用C
,一般包含下面几步:
Java
类中声明本地方法C
代码实现声明的本地方法Java
类中静态代码块调用System.loadLibrary
加载so库
Java
中HelloWorld
类
package com.example.jnifirst
class HelloWorld {
fun main(args:Array<String>) {
Log.d(TAG, stringFromJNI())
}
/**
* jni生成字符串返回给java
*/
private external fun stringFromJNI(): String
companion object {
const val TAG = "HelloWorld"
// Used to load the 'jnifirst' library on application startup.
init {
System.loadLibrary("jnifirst")
}
}
}
JNI类
#include <jni.h>
#include <string.h>
/**
* @param env 接口指针,JNI环境,指向一个个函数表,函数表中的每一个入口指向一个JNI函数。本地方法通过这些函数访问JVM中的数据结构
* @param thiz java中定义这个方法的对象。会根据本地方法是一个静态方法还是实例方法有所不同。静态方法的话,会是本地方法所在的类
* @return jstring java string对象
*/
JNIEXPORT jstring JNICALL
Java_com_example_jnifirst_HelloWorld_stringFromJNI(JNIEnv *env, jobject thiz) {
char *str = "hello world from jni";
//将c字符串转换成java字符串类型
return (*env)->NewStringUTF(env, str);
}
JNI
中的函数会生成一个字符串返回给Java
调用方
JNIEXPORT
和JNICALL
这两个宏定义在jni.h
中,确保整个函数在本地库外可见,并且C
编译器会进行正确的调用转换
第一个参数env
,接口指针,JNI
环境,指向一个个函数表,函数表中的每一个入口指向一个JNI
函数。本地方法通过这些函数访问JVM
中的数据结构
第二个参数thiz
,Java
中定义这个方法的对象。会根据本地方法是一个静态方法还是实例方法有所不同。静态方法的话,会是本地方法所在的类;实例方法的话就是定义这个方法的对象。
JNI
中与Java中函数名的对应的名称组成是:Java_package_className_methodName
。其中package包名中的点“.”会转换成下划线“_”如上述Java_com_example_jnifirst_MyString_stringFromJNI
jstring
代表了字符串,是JNI中对应的Java中的java.lang.String
。所有的Java
数据类型都会和JNI
中数据类型相对应。
JNI数据类型
基本类型
JNI类型 | Java类型 | 描述 |
---|---|---|
jbyte | byte | 有符号,8位,整型 |
jshort | short | 有符号,16位,整型 |
jint | int | 有符号,32位,整型 |
jlong | long | 有符号,64位,整型 |
jfloat | float | 32位,浮点型 |
jdouble | double | 64位,浮点型 |
jboolean | boolean | 无符号,8位,整型 |
jchar | char | 无符号,16位,整型 |
void | void | N/A |
引用类型
JNI类型 | Java类型 | 描述 |
---|---|---|
jclass | Class | 类 |
jobject | Object | Java对象 |
jstring | String | 字符串 |
jobjectArray | Object[] | 对象数组 |
jbyteArray | byte[] | byte数组 |
jshortArray | short[] | short数组 |
jintArray | int[] | int数组 |
jlongArray | long[] | long数组 |
jfloatArray | float[] | float数组 |
jdoubleArray | double[] | double数组 |
jbooleanArray | boolean[] | boolean数组 |
jcharArray | char[] | char数组 |
jthrowable | Throwable | Throwable |
JNI方法注册
当执行一个native方法时,JVM
需要直到该调用so中的哪个方法,这就需要用到方法注册了,通过注册,将native方法和so中的方法绑定起来,这样就能找到对应的方法了。
注册分为静态注和动态注册两种,默认是静态注册。
静态注册
如上述JNI
中方法,就是使用Java_package_className_methodName
,与Java
中定义的native方法绑定起来。
Java_com_example_jnifirst_MyString_stringFromJNI
动态注册
使用JNI
中定义的RegisterNatives
方法,进行方法注册,注册的时机是在JNI_OnLoad
时
#include <jni.h>
#include <string.h>
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
//获取env
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
//java中的类名,全限定
char *className = "com/example/jnifirst/JniOnLoad";
jclass c = (*env)->FindClass(env, className);
if (c == NULL) {
return JNI_ERR;
}
//声明本地方法数组,JNINativeMethod是JNI中的一个结构体
static const JNINativeMethod method[] = {
{"nativeFoo", "()V", (void *) nativeFoo}
};
// 注册本地方法
// clazz:指定的类,即 native 方法所属的类
// methods:方法数组,JNINativeMethod结构体数组
// nMethods:方法数组的长度
// 成功返回JNI_OK(0),失败返回负值
int rc = (*env)->RegisterNatives(env, c, method, sizeof(method) / sizeof(method[0]));
if (rc != JNI_OK) {
return rc;
}
return JNI_VERSION_1_6;
}
其中JNINativeMethod
结构体定义如下:
typedef struct {
const char* name; //native方法名
const char* signature; // 方法签名
void* fnPtr; //函数指针
} JNINativeMethod;
JNI类型签名
JNI
的类型签名标识了一个特定的Java
类型,这个类型可以是类、方法或数据类型。
类和对象的签名
组成为:L+包名+类名+;
,注意有一个“;” 。其中,包名中的“.”替换成“/”。
如String
类的签名就是:Ljava/lang/String;
基本数据类型的签名
一般就是基本数据类型的首字母大写,除了boolean
是Z,long
是J。对于void
,则是V。
Java类型 | 签名 |
---|---|
byte | B |
short | S |
int | I |
long | J |
float | F |
double | D |
boolean | Z |
char | C |
void | V |
数组的签名
-
一维数组
一维数组签名就是
[+数据类型
Java类型 签名 Object[] [Ljava/lang/Object String[] [Ljava/lang/String byte[] [B short[] [S int[] [I long[] [J float[] [F double[] [D boolean[] [Z char[] [C -
二维数组
二维数组签名就是
[[+数据类型
。如int[][]
就是[[I
方法的签名
方法的签名包括了方法参数和返回值信息。组成为:(所有参数的类型)返回值类型。
如
int fun1() ==> ()I
void fun2(int i) ==> (I)V
boolean fun3(int a, double b, String[] c) ==> (ID[Ljava/lang/String;)Z
参考资料: