前言
-
在 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 的作用
JavaVM
和 JNIEnv
是定义在 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 程序。