前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你应该了解的JNI知识(一)——静态注册与动态注册

你应该了解的JNI知识(一)——静态注册与动态注册

作者头像
用户1108631
发布2019-08-17 12:38:44
1.8K0
发布2019-08-17 12:38:44
举报

最近一直在做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的方法,比如说:

代码语言:javascript
复制
public native String sayHello();

得有一个JNI层的方法与之对应,这个对应规则是怎么样的呢?

这里简单来说有两种方式:

  • 静态注册:Java中的一个方法可以限定为:包名-类名-方法名-方法参数,这样可以唯一的确定一个方法;那么如果JNI层根据某种规则这样构造方法,是不是也一一对应了?这就是静态注册
  • 动态注册:上面类似一张静态表,但是如果每个JNI的方法与Java的代码有个映射表,只要将这张表告诉JVM,那就可以找到对应的C++方法了

静态注册

对于静态注册,JNI的方法命名规则为:

代码语言:javascript
复制
Java_packagename_classname_methodname(JNIEnv *env,jclass/jobject,...)  
javac、javah

Java是提供了工具来生成JNI的头文件的,步骤是:

  1. javac编译得到class文件
  2. javah编译class得到头文件,命名规则是packagename_classname.h 编译得到的头文件如下:
代码语言:javascript
复制
/* 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 __cplusplusextern "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

可以看到方法签名是:

代码语言:javascript
复制
JNIEXPORT return_type JNICALL Java_packagename_classname_methodname(JNIEnv *,jobject,..)

这里是头文件,只是标识方法签名。

注意点:这里第二个参数是jobject类型,这是因为Java的sayHello()是一个对象方法,而如果是一个实例方法(static修饰的),这里第二个参数就会是jclass

这里只是头文件,下面建一个源文件,引入该头文件,实现方法的具体实现。

实现如下:

代码语言:javascript
复制
#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端方法的关键,其定义如下:
代码语言:javascript
复制
typedefstruct{constchar* name;constchar* signature;void*       fnPtr;}JNINativeMethod;
  • name:JNI层的方法名
  • signature:Java层的方法签名
  • fnPtr:JNI层的函数指针 比如上面的例子使用动态注册的实现如下:
代码语言:javascript
复制
#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方法,其对应的方法签名是怎样的呢?

关于方法签名有几个要点:

  • 结构是(returntype)paramtertype,方法返回类型+参数类型
  • 如果类型是引用类型,那么表示为Lpackagename/classname,比如String表示为Ljava/lang/String
  • *如果是引用类型,需要加";"进行分割 *
  • 如果是数组类型,用[类型表示,比如byte[]表示为[B

基本类型的符号对应表如下图:

Z对应boolean,其他都是首字母

javap

Java提供了方法可以看一个方法的签名的工具:javap。

对应于一个class文件,使用

代码语言:javascript
复制
javap -s classname

以上面的例子为例,执行上述命令,得到的结果如下:

代码语言:javascript
复制
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,所以编译命令如下

代码语言:javascript
复制
 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层如何互相作用,敬请期待。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天学点Android知识 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JNI是什么
    • 数据类型
      • 方法匹配
        • 静态注册
        • 动态注册
        • 静态注册与动态注册的区别
      • 编译
        • 使用
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档