【Android -- JNI 和 NDK】认识 JNI

前言

  • 在 Android 生态中主要有 C/C++、Java、Kotlin 三种语言 ,它们的关系不是替换而是互补。其中,C/C++ 的语境是算法和高性能,Java 的语境是平台无关和内存管理,而 Kotlin 则融合了多种语言中的优秀特性,带来了一种更现代化的编程方式;

  • JNI 是实现 Java 代码与 C/C++ 代码交互的特性, 思考一个问题 —— Java 虚拟机是如何实现两种毫不相干的语言的交互的呢? 今天,我们来全面总结 JNI 开发知识框架,为 NDK 开发打下基础。

认识 JNI

1. 为什么要使用 JNI ?

JNI(Java Native Interface,Java 本地接口)是 Java 生态的特性,它扩展了 Java 虚拟机的能力,使得 Java 代码可以与 C/C++ 代码进行交互。 通过 JNI 接口,Java 代码可以调用 C/C++ 代码,C/C++ 代码也可以调用 Java 代码。

2. JNI 开发的基本流程

一个标准的 JNI 开发流程主要包含以下步骤:

  • 1、创建 HelloWorld.java,并声明 native 方法 sayHi();
  • 2、使用 javac 命令编译源文件,生成 HelloWorld.class 字节码文件;
  • 3、使用 javah 命令导出 HelloWorld.h 头文件(头文件中包含了本地方法的函数原型);
  • 4、在源文件 HelloWorld.cpp 中实现函数原型;
  • 5、编译本地代码,生成 Hello-World.so 动态原生库文件;
  • 6、在 Java 代码中调用 System.loadLibrary(…) 加载 so 文件;
  • 7、使用 Java 命令运行 HelloWorld 程序。

JNI 模板代码

JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi (JNIEnv *, jobject);

1. JNI 函数名

为什么 JNI 函数名要采用 Java_com_xurui_HelloWorld_sayHi 的命名方式呢?—— 这是 JNI 函数静态注册约定的函数命名规则。Java 的 native 方法和 JNI 函数是一一对应的映射关系,而建立这种映射关系的注册方式有 2 种:静态注册 + 动态注册。

其中,静态注册是基于命名约定建立的映射关系,一个 Java 的 native 方法对应的 JNI 函数会采用约定的函数名,即 Java_[类的全限定名 (带下划线)]_[方法名]

2. 关键词 JNIEXPORT

JNIEXPORT 是宏定义,表示一个函数需要暴露给共享库外部使用时。JNIEXPORT 在 Window 和 Linux 上有不同的定义:

// Windows 平台 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)

// Linux 平台:
#define JNIIMPORT
#define JNIEXPORT  __attribute__ ((visibility ("default")))

3. 关键词 JNICALL

JNICALL 是宏定义,表示一个函数是 JNI 函数。JNICALL 在 Window 和 Linux 上有不同的定义:

// Windows 平台 :
#define JNICALL __stdcall // __stdcall 是一种函数调用参数的约定 ,表示函数的调用参数是从右往左。

// Linux 平台:
#define JNICALL

4. 参数 jobject

jobject 类型是 JNI 层对于 Java 层应用类型对象的表示。每一个从 Java 调用的 native 方法,在 JNI 函数中都会传递一个当前对象的引用。区分 2 种情况:

  • 1、静态 native 方法: 第二个参数为 jclass 类型,指向 native 方法所在类的 Class 对象;
  • 2、实例 native 方法: 第二个参数为 jobject 类型,指向调用 native 方法的对象。

5. JavaVM 和 JNIEnv 的作用

JavaVMJNIEnv 是定义在 jni.h 头文件中最关键的两个数据结构:

  • JavaVM: 代表 Java 虚拟机,每个 Java 进程有且仅有一个全局的 JavaVM 对象,JavaVM 可以跨线程共享;
  • JNIEnv: 代表 Java 运行环境,每个 Java 线程都有各自独立的 JNIEnv 对象,JNIEnv 不可以跨线程共享。

JavaVM 和 JNIEnv 的类型定义在 C 和 C++ 中略有不同,但本质上是相同的,内部由一系列指向虚拟机内部的函数指针组成。

实例

1、创建 HelloWorld.java,并声明 native 方法 hello_world();

public class JNIExample {
    
    

    static {
    
    
        // 函数System.loadLibrary()是加载dll(windows)或so(Linux)库,只需名称即可,
        // 无需加入文件名后缀(.dll或.so)
        System.loadLibrary("JNIExample");
        init_native();
    }

    private static native void init_native();

    public static native void hello_world();

    public static void main(String...args) {
    
    
        JNIExample.hello_world();
    }
}

2、使用 javac 命令编译源文件,生成 HelloWorld.class 字节码文件;

javac com/kevin/jni/JNIExample.java
javah com.kevin.jni.JNIExample

3、使用 javah 命令导出 HelloWorld.h 头文件(头文件中包含了本地方法的函数原型);

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_kevin_jni_JNIExample */

#ifndef _Included_com_kevin_jni_JNIExample
#define _Included_com_kevin_jni_JNIExample
#ifdef __cplusplus
extern "C" {
    
    
#endif
/*
 * Class:     com_kevin_jni_JNIExample
 * Method:    init_native
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_kevin_jni_JNIExample_init_1native
  (JNIEnv *, jclass);

/*
 * Class:     com_kevin_jni_JNIExample
 * Method:    hello_world
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_kevin_jni_JNIExample_hello_1world
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

4、在源文件 HelloWorld.cpp 中实现函数原型;

void init_native(JNIEnv *env, jobject thiz) {
    
    
    printf("native_init\n");
    return;
}

void hello_world(JNIEnv *env, jobject thiz) {
    
    
    printf("Hello World!");
    return;
}

static const JNINativeMethod gMethods[] = {
    
    
        {
    
    "init_native", "()V", (void*)init_native},
        {
    
    "hello_world", "()V", (void*)hello_world}
};

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    
    
    __android_log_print(ANDROID_LOG_INFO, "native", "Jni_OnLoad");
    JNIEnv* env = NULL;
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) // 从 JavaVM 获取JNIEnv,一般使用 1.4 的版本
        return -1;
    jclass clazz = env->FindClass("me/shouheng/jni/JNIExample");
    if (!clazz){
    
    
        __android_log_print(ANDROID_LOG_INFO, "native", "cannot get class: com/example/efan/jni_learn2/MainActivity");
        return -1;
    }
    if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0])))
    {
    
    
        __android_log_print(ANDROID_LOG_INFO, "native", "register native method failed!\n");
        return -1;
    }
    return JNI_VERSION_1_4;
}

5、编译本地代码,生成 Hello-World.so 动态原生库文件;

gcc -c -I"E:\JDK\include" -I"E:\JDK\include\win32" jni/JNIExample.c

6、在 Java 代码中调用 System.loadLibrary(…) 加载 so 文件;

gcc -Wl,--add-stdcall-alias -shared -o JNIExample.dll JNIExample.o

7、使用 Java 命令运行 HelloWorld 程序。

猜你喜欢

转载自blog.csdn.net/duoduo_11011/article/details/131299772