你应该了解的JNI知识——静态注册与动态注册
Posted xingfeng_coder
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你应该了解的JNI知识——静态注册与动态注册相关的知识,希望对你有一定的参考价值。
最近一直在做native这边的跨平台开发,整个结构基本就是下图:
大体说来就是,底层C/C++代码。那么对于两端分别有不同的处理:
-
对于android端而言,由于需要给Java端使用,因此需要提供JNI接口,然后将整个的代码打包编译成.so给Android端使用
-
对于ios端,由于oc是可以直接调用c的,但是需要将代码打包编译成iOS需要的Framework,然后由于需要给iOS端使用,需要将头文件暴露出来,其实是类似JNI接口的(给别人用,总得让别人知道怎么用,对不对?)
由于我是做Android的,因此重点关注JNI,主要是总结应该知道的一些JNI知识。
JNI是什么
Java是支持调用C/C++代码的,不过不能直接调用,因此需要一个中间层来进行转换、翻译,这就是JNI(Java Native Interface)的意思,JNI的作用就是粘合Java代码和C++代码。
数据类型
JNI是Java特有的东西,是为了打通Java和C/C++代码的一种工具,因此其即不同于Java,又不同于C/C++。
我们知道,Java的数据类型分为基本数据类型和引用数据类型,JNI也是与之对应的。
基本数据类型的对应关系如下图:
对于八种基本数据类型,每种对应关系是xxx(Java类型)——>jxxx(JNI类型)
而对于引用类型,JNI的类型是jobject,继承的类型有多种,比如String、Class、数组、异常等,如下图:
可以看到,JNI的数据类型和Java的数据类型的对应关系是比较好理解和记忆的。
方法匹配
对于一个native的方法,比如说:
public native String sayHello();
得有一个JNI层的方法与之对应,这个对应规则是怎么样的呢?
这里简单来说有两种方式:
- 静态注册:Java中的一个方法可以限定为:包名-类名-方法名-方法参数,这样可以唯一的确定一个方法;那么如果JNI层根据某种规则这样构造方法,是不是也一一对应了?这就是静态注册
- 动态注册:上面类似一张静态表,但是如果每个JNI的方法与Java的代码有个映射表,只要将这张表告诉JVM,那就可以找到对应的C++方法了
静态注册
对于静态注册,JNI的方法命名规则为:
Java_packagename_classname_methodname(JNIEnv *env,jclass/jobject,...)
javac、javah
Java是提供了工具来生成JNI的头文件的,步骤是:
-
javac编译得到class文件
-
javah编译class得到头文件,命名规则是packagename_classname.h
编译得到的头文件如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xingfeng_HelloWorld */
#ifndef _Included_com_xingfeng_HelloWorld
#define _Included_com_xingfeng_HelloWorld
#ifdef __cplusplus
extern "C"
#endif
/*
* Class: com_xingfeng_HelloWorld
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_xingfeng_HelloWorld_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
#endif
#endif
可以看到方法签名是:
JNIEXPORT return_type JNICALL Java_packagename_classname_methodname(JNIEnv *,jobject,..)
这里是头文件,只是标识方法签名。
注意点:这里第二个参数是jobject类型,这是因为Java的sayHello()是一个对象方法,而如果是一个实例方法(static修饰的),这里第二个参数就会是jclass
这里只是头文件,下面建一个源文件,引入该头文件,实现方法的具体实现。
实现如下:
#include "com_xingfeng_HelloWorld.h"
extern "C"
JNIEXPORT jstring JNICALL Java_com_xingfeng_HelloWorld_sayHello
(JNIEnv *env, jobject thiz)
return env->NewStringUTF("Hello,This is from jni");
这里,就是使用JNIEnv创建了一个字符串,并返回了。
动态注册
动态注册的关键字是两个:
-
JNI_OnLoad()方法,这个是载入Jni库后调用的第一个方法,在这里可以将方法对应表注册给JNI环境
-
JNINativeMethod结构,这个结构是将jni层的方法映射到Java端方法的关键,其定义如下:
typedef struct const char* name; const char* signature; void* fnPtr; JNINativeMethod;
- name:JNI层的方法名
- signature:Java层的方法签名
- fnPtr:JNI层的函数指针
比如上面的例子使用动态注册的实现如下:
#include <jni.h>
#include <string.h>
jstring dynamic(JNIEnv *env,jobject thiz)
return env->NewStringUTF("Hello,this is from jni");
//方法对应表
const static JNINativeMethod methods[]=
"dynamic","()Ljava/lang/String;",(void *)dynamic
;
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void* reserved)
JNIEnv *env=NULL;
if(vm->GetEnv((void **) &env,JNI_VERSION_1_4)!=JNI_OK)
return JNI_FALSE;
jclass jclazz=env->FindClass("com/xingfeng/HelloWorld");
if(env-> RegisterNatives(jclazz,methods, sizeof(methods)/ sizeof(methods[0]))<0)
return JNI_FALSE;
return JNI_VERSION_1_4;
这里JNI_OnLoad里面的方法的模板是一样的,区别是FindClass()的参数,这里的参数就是那个类名,类名+方法名才能唯一确定一个接口。
注意点:FindClass()中的结构使用"/"来区别包名的,比如com.xingfeng.HelloWorld,在这里就要变成com/xingfeng/HelloWorld了
方法签名
JNINativeMethod结构体中第二个参数对应于Java的方法签名,那么对于一个Java方法,其对应的方法签名是怎样的呢?
关于方法签名有几个要点:
- 结构是(paramter_type)return_type,(参数类型)方法返回类型
- 如果类型是引用类型,那么表示为Lpackagename/classname,比如String表示为Ljava/lang/String
- **如果是引用类型,需要加";"进行分割 **
- 如果是数组类型,用[类型表示,比如byte[]表示为[B
基本类型的符号对应表如下图:
Z对应boolean,其他都是首字母
javap
Java提供了方法可以看一个方法的签名的工具:javap。
对应于一个class文件,使用
javap -s classname
以上面的例子为例,执行上述命令,得到的结果如下:
Compiled from "HelloWorld.java"
public class com.xingfeng.HelloWorld
public com.xingfeng.HelloWorld();
descriptor: ()V
public native java.lang.String sayHello();
descriptor: ()Ljava/lang/String;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
static ;
descriptor: ()V
其中descriptor就是对应的签名,比如方法"sayHello"对应的方法签名就是"()Ljava/lang/String;"
静态注册与动态注册的区别
区别是效率。静态注册,每次使用native方法时,都要去寻找;而动态注册,由于有张表的存在,因此查找效率高。
编译
上面不管是静态注册方法,还是动态注册方法,都需要将cpp文件编译成平台所需要的库。不同平台,库的表现形式是不一样的,
- Linux平台,编译生成.so库(终于知道Android平台为啥需要.so库了吧?)
- Mac平台,编译生成.jnilib
- Windows平台,编译生成.dll(用过Windows的应该都知道这个了)
由于我目前是使用的Mac,所以编译命令如下
gcc -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/include/darwin -fPIC ../jni/DynamicHelloWorld.cpp -o libdynamic.jnilib
gcc编译参数说明:
- -shared:生成一个共享库,与静态库相对
- -I:指定需要包含的头文件
- -fPIC:作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code);这样一来,产生的代码中就没有绝对地址了,全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,都可以正确的执行 (参考自http://c.biancheng.net/view/2385.html)
- -o:输出的动态库名称
使用
使用我想每个人应该都会的,这里就不赘述了。
总结
上面主要是我自己从使用NDK开发中体会到的需要掌握的东西,最主要是静态注册与动态注册的实现。后面会介绍Java和JNI层如何互相作用,敬请期待。
关注我的技术公众号,不定期会有技术文章推送,不敢说优质,但至少是我自己的学习心得。微信扫一扫下方二维码即可关注:
以上是关于你应该了解的JNI知识——静态注册与动态注册的主要内容,如果未能解决你的问题,请参考以下文章