Java - 写给Android应用开发者的JNI快速入门指北

Java Native Interface(Java本地接口) 简称 JNI,是一种编程框架,使得 JVM 中的 Java程序 可以调用本地应用/或库,也可以被其他程序调用。其过程可以不负责任的理解成 Java 的反射,因为代码逻辑和反射调用很像。

源码

本文所有涉及的所有源码:https://github.com/gavinliu/Study-JNI

平台 编译环境
macOS gcc

HelloStringFromC

在java中编写native方法

HelloStringFromC.java
1
2
3
4
5
6
7
8
public class HelloStringFromC {
public static native String helloStringFromC();
public static void main(String[] args) {
}
}

生成jni头文件

1
javac -jni HelloStringFromC

将会生成 HelloStringFromC.h

HelloStringFromC.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloStringFromC */
#ifndef _Included_HelloStringFromC
#define _Included_HelloStringFromC
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloStringFromC
* Method: helloStringFromC
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_HelloStringFromC_helloStringFromC
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif

编写native代码

HelloStringFromC.c
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
#include "HelloStringFromC.h"
JNIEXPORT jstring JNICALL Java_HelloStringFromC_helloStringFromC(JNIEnv* env, jclass cls) {
return (*env)->NewStringUTF(env, "Hello");
}

编译so库

1
gcc HelloStringFromC.c -fPIC -shared -o libhelloString.so

java调用

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloStringFromC {
public static native String helloStringFromC();
public static void main(String[] args) {
System.out.println(helloStringFromC());
}
static {
System.load("/Users/gavin/Workspace/Study-JNI/01.HelloStringFromC/libhelloString.so");
}
}
1
> Hello

数据类型

基本数据类型

Java 数据类型 JNI 数据类型 C 数据类型
blean jboolean unsigned char
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint int
long jlong long long
float jfloat float
double jdouble double

引用类型

Java 数据类型 JNI 数据类型
Object jobject
Class jclass
String jstring
array[] jarray

数组类型

Java 数据类型 JNI 数据类型
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double jdoubleArray
objcet jobjectArray

JNIEnv

1
2
struct JNINativeInterface_;
typedef const struct JNINativeInterface_ *JNIEnv;

JNIEnv 其实是 JNINativeInterface_ 的指针别名,这个结构是C和Java交互的关键类,提供了C数据类型和JNI数据类型的互相转换和C调用Java的相关方法等。

Java 调 C

Java调C的套路:

  1. 在Java源码中编写native方法
  2. 使用 javah 生成 jni 头文件
  3. 实现 jni 方法
  4. 编译生成 so 动态链接库
  5. 在Java中使用动态链接库

静态调用

1
2
3
4
public static native String getFullName(String firstName);
// ==>
JNIEXPORT jstring JNICALL Java_CallC_getFullName
(JNIEnv *, jclass, jstring);

静态调用生成的JNI方法,第二个参数为 jclass,代表Java中 CallC.class 这个对象。

对象调用

1
2
3
4
public native String getFirstName(String[] fullName);
// ==>
JNIEXPORT jstring JNICALL Java_CallC_getFirstName
(JNIEnv *, jobject, jobjectArray);

对象调用生成的JNI方法,第二个参数为 jobject,代表Java中 CallC 这个对象。

示例

  1. Java 调 C 传入一个字符串,返回拼接后的新字符串
  2. Java 调 C 传入一个字符串数组,返回数组第一个元素
CallC.c
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "CallC.h"
JNIEXPORT jstring JNICALL Java_CallC_getFullName
(JNIEnv* env, jclass jcl, jstring firstName) {
// jstring -> char*
const char* fn = (*env)->GetStringUTFChars(env, firstName, JNI_FALSE);
const char* ln = " Liu";
char src[50], dest[50];
strcpy(src, ln);
strcpy(dest, fn);
// C的拼接字符串
strcat(dest, src);
return (*env)->NewStringUTF(env, dest);
}
JNIEXPORT jstring JNICALL Java_CallC_getFirstName
(JNIEnv* env, jobject jobj, jobjectArray fullName) {
// 从 对象数组中获取元素
jstring elm = (jstring) (*env)->GetObjectArrayElement(env, fullName, 0);
return elm;
}
CallC.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CallC {
public static native String getFullName(String firstName);
public native String getFirstName(String[] fullName);
public static void main(String[] args) {
System.out.println(getFullName("Gavin"));
String[] fullName = {"Gavin", "Liu"};
System.out.println(new CallC().getFirstName(fullName));
}
static {
System.load("/Users/gavin/Workspace/Study-JNI/02.Java2C/libcallC.so");
}
}
1
2
> Gavin Liu
> Gavin

C 调 Java

C调Java的套路:

  1. 获取 FieldID 或者 MethedID
  2. 通过 ID 可以 GET|SET Field,和调用 Methed

其中第一步需要用到 Field 和 Methed 签名,可以用 javap 命令获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
javap -s -p CallJava
Compiled from "CallJava.java"
public class CallJava {
public java.lang.String firstName;
descriptor: Ljava/lang/String;
public CallJava();
descriptor: ()V
public native java.lang.String changeName();
descriptor: ()Ljava/lang/String;
public native java.lang.String sayHiFromC();
descriptor: ()Ljava/lang/String;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
public void sayHiFromJava();
descriptor: ()V
static {};
descriptor: ()V
}

示例

  1. C 改变 Java 中的成员变量
  2. C 调用 Java 方法
CallJava.c
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
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "CallJava.h"
JNIEXPORT jstring JNICALL Java_CallJava_changeName
(JNIEnv * env, jobject obj) {
// 获取 jclass
jclass cls = (*env)->GetObjectClass(env, obj);
// 获取 fieldID
jfieldID firstNameID = (*env)->GetFieldID(env, cls, "firstName", "Ljava/lang/String;");
// 获取 属性
jstring firstName = (*env)->GetObjectField(env, obj, firstNameID);
// 拼接新字符串
const char* str = (*env)->GetStringUTFChars(env, firstName, JNI_FALSE);
char src[50] = "super ";
strcat(src, str);
// 设置 属性
jstring newName = (*env)->NewStringUTF(env, src);
(*env)->SetObjectField(env, obj, firstNameID, newName);
return newName;
}
JNIEXPORT void JNICALL Java_CallJava_sayHiFromC
(JNIEnv * env, jobject obj) {
// 获取 jclass
jclass cls = (*env)->GetObjectClass(env, obj);
// 获取 methodID
jmethodID methodID = (*env)->GetMethodID(env, cls, "sayHiFromJava", "()V");
// 调用方法
(*env)->CallVoidMethod(env, obj, methodID);
printf("%s\n", "sayHiFromC");
}
CallJava.java
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
public class CallJava {
static {
System.load("/Users/gavin/Workspace/Study-JNI/03.C2Java/libcallJava.so");
}
public native String changeName();
public native void sayHiFromC();
public String firstName = "Gavin";
public static void main(String[] args) {
CallJava c = new CallJava();
System.out.println("c.firstName \t" + c.firstName);
System.out.println("c.changeName() \t" + c.changeName());
System.out.println("c.firstName \t" + c.firstName);
c.sayHiFromC();
}
public void sayHiFromJava() {
System.out.println("sayHiFromJava");
}
}
1
2
3
4
5
c.firstName Gavin
c.changeName() super Gavin
c.firstName super Gavin
sayHiFromJava
sayHiFromC

其他

JNIEnv 中还有很多方法,就不作一一介绍,掌握了其套路,代码就很容易写了,比如说调用方法是CallVoidMethod,调用静态方法就是CallStaticVoidMethod

C & C++ 的一些不同

JNIEnv 对象不一样

在C中:JNIEnv* 是一个二级指针,所以是使用 (*env)->
在C++中:JNIEnv* 是一个一级指针,则是使用 env->

引用类型不一样

在C中:是使用结构体实现
在C++中:是使用对象实现

官方文档

http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

Gavin Liu wechat
欢迎您扫一扫上面的二维码,订阅我的微信公众号!