你应该了解的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的头文件的,步骤是:

  1. javac编译得到class文件

  2. 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知识——静态注册与动态注册的主要内容,如果未能解决你的问题,请参考以下文章

你应该了解的JNI知识——Java与JNI互相调用

你应该了解的JNI知识——Java与JNI互相调用

JNI知识点总结

22 JNI - 动态注册与JNI线程

转深入了解android平台的jni---注册native函数

JNI方法注册源码分析(JNI_OnLoad|动态注册|静态注册|方法替换)