JNI学习笔记

Posted by Chejdj Blog on May 24, 2019

    本来预定这篇博客应该是4月份的博客,因为中间有事去出去实习了,所以4月份,5月份两篇博客还欠着,先写4月份的,这篇博客主要记录一下自己在学习JNI要点笔记。

理解几个概念

(1) JNI是什么? JNI全称Java Native Interface,就是Java的本地接口,这个接口的目的是让Java可以和本地其他语言进行交互(c/c++)
(2) NDK是什么?
NDK 全称 Native Development Kit. 是一个本地开发的工具包。目的就是自动将c/c++包成动态库与应用一起打包到apk中去。
(3) JavaVm是什么?
JavaVm是虚拟机在JNI层代表,一个进程只有一个JavaVM,所有线程共用一个JavaVM
(4)JNIEnv是什么?
JNIEnv表示Java调用native语言的环境,封装了很多JNI方法的指针,只在创建它的线程生效,不能跨线程传递,不同线程JNIEnv彼此独立

初始化项目讲解

在AndroidStudio创建一个JNI开发项目的时候,会默认生成一个函数public native String stringFromJNI(),然后在cpp文件夹下面生成一个native-lib.cpp文件,里面有stringFromJNI()方法的c++的实现,在c++层返回一个“hello world”字符。 代码见下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//MainActivity.java
public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");   //(1)
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();  (2)
}

//native-lib.cpp
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_chejdj_ndk_MainActivity_stringFromJNI(   //(3)
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

这里需要注意的是,代码中我标记的3处
(1)处在静态代码块中加载了名字“native-lib”的动态库,System.loadLibray()就是用来本地库,至于这个“native-lib”如何来的?后面再讲。
(2)处,定义一个本地方法,Java调用本地方法,调用的该方法使用native关键字 (3)处,(2)中方法的本地具体实现,注意这里的函数名字为Java_+具体的包名+类名+函数名称。
看到这里我们可能会有几个疑问?

  1. 为什么要在静态代码块中加载“native-lib”动态库?
  2. 在Java层定义了一个native方法,我在c++层实现了这个方法的具体实现,我怎么知道我调用的就是我在c++层的那个函数呢?
    带着这两个问题,我们慢慢深入它

为什么加载native-lib库?

首先,我们要知道Java JNI使用场景是:
(1) Java库无法提供基于平台系统相关特性的功能
(2) 已经用其他语言写好库,需要Java调用,不想重新编码实现,想直接复用它们
(3) 希望实现时间和性能要求比较高逻辑,视频或者图片处理
Java JNI调用的是一个本地库,库又分成两种:静态链接库和动态链接库。

  • 静态链接库:静态库对函数库的链接放在编译时期完成,比较浪费空间和资源
  • 动态链接库: 动态库把一些库函数链接载入推迟到了程序运行的时期,可以实现进程之间的资源共享。

我们使用NDK的目的就是把我们的c/c++代码编译成动态或者静态链接库,一般我们都是编译成动态链接库,原因也是上面提到的静态链接库的缺点。
那么如何编译生成这个库呢?我们必须了解一个东西。AndroidStudio使用Cmake来编译我们的c/c++代码,它是一种跨平台编译工具。Cmake的使用主要就是通过编写CMakeLists.txt文件,来指导c/c++程序项目的编译。主要过程就是
(1)编写CMakeLists.txt文件(需要自己写配置)
(2)用cmake命令将CMakeLists.txt文件转成make所需要的makefile文件(自动)
(3)用make命令最终编译c/c++代码生成可执行程序或共享库
下面是CMakeLists.txt代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cmake_minimum_required(VERSION 3.4.1)

add_library(
        native-lib
        SHARED
        native-lib.cpp)    //(1)

find_library(
        log-lib             //(2)
        log)

target_link_libraries(     //(3)
        native-lib
        ${log-lib})

(1)处,创建一个库,第一个参数为库的名称,第二个是库类型SHAED和STATIC,SHARED代表动态库(以.so为后缀),STATIC静态库(以.a为后缀)。第三个参数是库的源代码路径,最终会把这个源代码编译成native-lib动态库
(2)处find_libary查找某个库,查找路径是你NDK安装路径下面提供一些默认的库,这里的log库是可以像在Java使用Log打印日志一样。
(3)处target_link_libaries:把log-lib库链接进native-lib,这样native-lib的源代码中就可以使用log-lib中的库了
经过我们在CMakeLists.txt中配置,我们就会编译成一个native-lib.so动态库。
那我们要在Java中使用这个库中的函数,就需要先将这个库加载入Java虚拟机中,所以需要加载该库。

在Java中native方法如何映射到c/c++方法?

虚拟机如何知道该调用哪个so方法?这里需要用到注册的概念,通过函数映射表将指定native方法和so对应方法绑定起来,这样就可以找到指定的方法。注册分为两种:静态注册和动态注册,默认的是静态注册

静态注册方法

再贴一下上面的代码

1
2
3
4
5
6
7
extern "C" JNIEXPORT jstring JNICALL
Java_com_chejdj_ndk_MainActivity_stringFromJNI(   //(3)
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

加入extern "C"目的是

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

要想正确的实现静态注册,具有一下步骤:

  • 通过JNIEXPORT 和JNICALL 两个宏定义声明,在虚拟机加载so的时候,它就会把这个函数链接到对应native方法,中间的是函数返回值,和普通的变量类型不一样,需要返回虚拟机预定的变量(通常在C变量类型前面加一个j就行)。
  • 对应规则是: Java+包名+类名+方法名,通过这个就可以找到Java中定义的native方法
    当然我们有更简单的方法,不用每次都写这么长的方法名称,可以使用javah命令生成对应的.h文件,拷贝到cpp的源码工程里面去,然后实现其中的方法就行
动态注册方法

主要利用RegisterNatives方法自己手动的完成native方法和so中方法绑定,虚拟机就可以直接通过函数映射表找到对应函数了,不需要通过Java方法名查找匹配的Native函数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jstring stringFromJNI(JNIEnv *env, jobject instance) {
    std::string hello = "Hello,world";
    return env->NewStringUTF(hello.c_str());
}
jint RegisterNatives(JNIEnv *env, char *class_path) {
    jclass clazz = env->FindClass(class_path);
    if (clazz == NULL) {
        LOGE("%s", "can't find class com/chejdj/ndk_demo/MainActivity");
    }
    JNINativeMethod methods[] = {
            {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI}}
    };
    return env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    jint result = RegisterNatives(env, "com/chejdj/ndk_demo/MainActivity");
    LOGE("RegisterNative result: %d", result);
    return JNI_VERSION_1_6;
}

上面的代码中就完成了c/c++层 stringFromJNI方法和MainActivity的方法绑定。需要注意这里的本地方法名字无所谓,不需要按照规则写,不需要声明JNIEXPORT和JNICALL。
这里关键信息就是在重载JNI_OnLoad()函数中调用env->RegisterNatives完成方法的注册。JNI_OnLoad()看名字我们就知道当我们调用System.loadLibrary的时候虚拟机首先就会去执行这个函数。
下面介绍一下env->RegisterNatives方法
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
参数:

  • clazz: 指定类,native方法所属的类
  • mehtods:方法数组
  • nMethods: 方法数组的长度
    JavaNativeMethod 数据结构结构是
1
2
3
4
5
typedef struct {
    const char* name; //java native方法名字
    const char* signature; //方法签名例如 "()Ljava/lang/String;"
    void*       fnPtr;    //对应的函数指针,就是c代码实现函数
} JNINativeMethod;

正确填充就可以实现动态注册了

静态注册和动态注册优缺点

JNI静态注册缺点

  1. native函数名称长
  2. 第一次调用时需要根据函数名建立索引,影响效率,需要先根据函数名在JNI层搜索本地函数,然后建立对应关系,而动态注册直接保存的是就是映射关系
  3. JNI层函数名是由java接口名生成,很容易通过hook调用动态库中函数
    最好优先使用动态注册
    这里你可能会注意到方法签名是什么东西,注意JNINativeMethod的第三个参数?,下面介绍一下JNI的方法签名。

    方法签名

    JNI就是通过方法签名来识别是哪一个Java方法的,因为Java支持方法重载,仅靠类名和函数名是没有方法确定一个一个函数的,所以JNI提供类一套所谓的“签名规则”,用一个字符串描述一个方法,能够唯一确定该方法。
    方法规则

    JNI调用Java方法

    这里具体又分实体方法还是静态方法,我写一个调用实例方法例子,具体就是看API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    void callJavaInstanceMethod(JNIEnv *env,jobject cls){
     jclass clazz =NULL;
     jobject jobj = NULL;
     jmethodID  mid_construct =NULL;
     jmethodID  mid_instance=NULL;
     jstring str_arg = NULL;
     //1. 从classpath路径下搜索ClassMethod类,并返回这个类的Class对象
     clazz = env->FindClass("com/chejdj/ndk_demo/MainActivity");
     if(clazz ==NULL){
         printf("can't find class com/chejdj/ndk_demo/MainActivity");
         return;
     }
     //2. 获取类的默认构造方法ID
     mid_construct = env->GetMethodID(clazz,"MainActivity","()");
     if(mid_construct ==NULL){
         printf("can't find construct method");
         return ;
     }
     //3.查找实例方法ID
     mid_instance = env->GetMethodID(clazz,"printHello","()V");
     if(mid_instance ==NULL){
         return ;
     }
     //4. 创建该类实例
     jobj = env->NewObject(clazz,mid_construct);
     if(jobj==NULL){
         printf("can't create  instance");
         return ;
     }
     //5.调用对象实例方法
     env->CallVoidMethod(jobj,mid_instance);
    
     //6. 删除局部引用
     env->DeleteLocalRef(clazz);
     env->DeleteLocalRef(jobj);
    }