JNI是Android应用开发中不太常涉及的技术,但在Framework层中却被广泛使用。作为一名Android应用开发人员,学习JNI知识,对理解整个系统原理还是有很大帮助的。
学习JNI有很多途径:
因此这篇文章探讨的主题就是在最小环境下,进行JNI的学习与验证。JNI是Java本身就具备的能力,所以最小环境就是Java运行的环境,不需要任何Android开发工具以及IDE。
为了方便调试与编译,这里我使用Ubuntu系统,需要安装的开发工具有:
apt-get install build-essential
安装在这个JNI例子中,主要做这几件事:
Java层的代码很简单,声明一个String变量,供native代码读写。同时提供一个打印变量的方法
public class JniStudy {
private String msg = "Hello World from Java";
private native void nativeChangeMsg();
public native void nativeCallPrintMsg();
public void getNativeMsg() {
nativeChangeMsg();
}
public void printMsg() {
System.out.println(this.msg);
}
public static void main(String[] args) {
JniStudy test = new JniStudy();
test.getNativeMsg(); // 调用native方法更改msg内容
test.printMsg();
System.out.println("####################");
test.nativeCallPrintMsg(); // 调用native方法,native再反向调用printMsg方法
}
}
native
标识的两个方法,都是JNI的方法,稍后需要通过命令生成.h
头文件。其他代码均是基础的Java代码。
保存文件后,首先通过javac
命令直接编译,应当可以编译成功。
javac JniStudy.java
接下来需要使用Java编译工具生成JNI模块的.h
头文件。在旧版本Java中,需要使用javah
,Java11中已经去掉这个命令了,直接使用javac
即可:
javac JniStudy.java -h .
-h
参数用来生成头文件,.
代表生成到当前路径下。
生成的头文件如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class JniStudy */
#ifndef _Included_JniStudy
#define _Included_JniStudy
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JniStudy
* Method: nativeChangeMsg
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniStudy_nativeChangeMsg
(JNIEnv *, jobject);
/*
* Class: JniStudy
* Method: nativeCallPrintMsg
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniStudy_nativeCallPrintMsg
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
然后在同一个目录下,创建一个C源码文件jniStudy.c
,编写native层逻辑
#include
#include "JniStudy.h" // 头文件在当前目录下,使用引号方式引入
void Java_JniStudy_nativeChangeMsg(JNIEnv *env, jobject obj) {
puts("JNI : Java_JniStudy_nativeChangeMsg");
// 获取 Java Class
jclass clazz = (*env)->FindClass(env, "JniStudy");
if (clazz == NULL) {
return;
}
// 通过 Java Class 获取变量 fieldID
jfieldID fieldId = (*env) -> GetFieldID(env, clazz, "msg", "Ljava/lang/String;");
if (fieldId == NULL) {
return;
}
// 通过 fieldID 来获取 Java 对象中的变量值
jstring msg_org = (*env) -> GetObjectField(env, obj, fieldId);
if (msg_org == NULL) {
return;
}
// 使用 GetStringUTFChars 生成C中的char字符串
const char * msg_char = (*env)->GetStringUTFChars(env, msg_org, NULL);
puts("Print Java String in JNI:");
puts(msg_char);
// 使用 NewStringUTF 在JNI侧生成 jstring
jstring msg = (*env) -> NewStringUTF(env, "Hello World from JNI");
// 通过 SetObjectField 方法,将 jstring 赋值到 Java 层相应的变量上(这里是Java中的msg)
(*env)->SetObjectField(env, obj, fieldId, msg);
}
void Java_JniStudy_nativeCallPrintMsg(JNIEnv *env, jobject obj) {
puts("JNI : Java_JniStudy_nativeCallPrintMsg");
// 获取 Java Class
jclass clazz = (*env)->FindClass(env, "JniStudy");
if (clazz == NULL) {
return;
}
// 通过 Java Class 获取变量 methodID
jmethodID printMethodId = (*env) -> GetMethodID(env, clazz, "printMsg", "()V");
if (printMethodId == NULL) {
return;
}
puts("JNI CallVoidMethod: printMsg");
// 通过 CallVoidMethod 调用 Java 中的 void 方法
(*env)->CallVoidMethod(env, obj, printMethodId);
}
Java_JniStudy_nativeChangeMsg
读取Java对象中的msg
,并打印;然后通过JNI接口更改msg
的内容,Java层通过printMsg
将新的内容打印出来。
Java_JniStudy_nativeCallPrintMsg
通过JNI的接口,直接调用Java层printMsg
方法。
详细的过程可参考代码注释。
编写好native层逻辑,就可以编译C代码了。编译需要两个头文件jni.h
,jni_md.h
。在头文件JniStudy.h
中,可以看到自动生成代码已经引用了jni.h
,jni_md.h
则是在jni.h
中引用的。
由于我们的代码路径下没有这两个文件,因此需要指定这两个文件路径,在Ubuntu中,这两个头文件分别位于
/usr/lib/jvm/java-11-openjdk-amd64/include/
/usr/lib/jvm/java-11-openjdk-amd64/include/linux/
我们需要把这两个路径添加到gcc编译参数中。
另外,由于我们编译的是库文件,不包含main函数,因此直接使用gcc -o
会报错
/usr/lib/gcc/x86_64-linux-gnu/7/…/…/…/x86_64-linux-gnu/Scrt1.o:在函数‘_start’中:
(.text+0x20):对‘main’未定义的引用
collect2: error: ld returned 1 exit status
需要添加-shared
参数
最后完整的编译命令如下:
gcc -shared -o libjnistudy JniStudy.c \
-I /usr/lib/jvm/java-11-openjdk-amd64/include/ \
-I /usr/lib/jvm/java-11-openjdk-amd64/include/linux
编译完成后会在同一个目录下生成名为libjnistudy
的库文件。
再次更改Java代码,载入我们编写的库文件。
由于默认的环境下,java的java.library.path
没有当前路径,所以这里使用System.load
方法,传入库文件的绝对路径
public class JniStudy {
......
static {
System.load("/home/myname/spc-work/jni-test/libjnistudy");
}
......
}
更改后再次使用javac
命令编译生成.class
文件
直接通过java命令运行:
java JniStudy
可以看到输出
JNI : Java_JniStudy_nativeChangeMsg
Print Java String in JNI:
Hello World from Java
Hello World from JNI
####################
JNI : Java_JniStudy_nativeCallPrintMsg
JNI CallVoidMethod: printMsg
Hello World from JNI
证明Java -> Native, Native -> Java 两条链路都打通了。
以下是我在调试中遇到的一些问题,有的在上面已经提到过,在这里再次汇总
javah
命令找不到这可能和JDK版本有关。旧版本JDK可以使用javah
命令。新版中(可能是JDK10以上)javac
命令已经集成了生成头文件功能。
/usr/lib/gcc/x86_64-linux-gnu/7/…/…/…/x86_64-linux-gnu/Scrt1.o:在函数‘_start’中:
(.text+0x20):对‘main’未定义的引用
collect2: error: ld returned 1 exit status
由于JNI加载的是库文件,没有main方法,编译时需要增加-shared
参数
gcc -shared xxx.c -o xxx
错误信息
A fatal error has been detected by the Java Runtime Environment
native层方法SetXXXField
,GetXXXField
等的传参不能为空,当代码编写错误,传入参数为空时就会崩溃。可以通过调试或打印信息,确定传参出错位置。
native层方法例如FindClass
,GetFieldID
,NewStringUTF
等等,其实都是有官方文档的:
JNI官方完整文档:Java Native Interface Specification Contents
JNI方法列表及文档:Chapter 4: JNI Functions
官网文档: Chapter 3: JNI Types and Data Structures
更简单的方法是通过javap
命令查看
例如JniStudy.class
文件
通过javap -s
可查看方法的签名
Compiled from "JniStudy.java"
public class JniStudy {
public JniStudy();
descriptor: ()V
public native void nativeCallPrintMsg();
descriptor: ()V
public void getNativeMsg();
descriptor: ()V
public void printMsg();
descriptor: ()V
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
static {};
descriptor: ()V
}
还可以通过javap -v
得到类文件编译后所有的信息,这个内容会比较多
......
Compiled from "JniStudy.java"
public class JniStudy
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // JniStudy
super_class: #15 // java/lang/Object
interfaces: 0, fields: 1, methods: 7, attributes: 1
Constant pool:
#1 = Methodref #15.#31 // java/lang/Object."":()V
#2 = String #32 // Hello World from Java
#3 = Fieldref #7.#33 // JniStudy.msg:Ljava/lang/String;
#4 = Methodref #7.#34 // JniStudy.nativeChangeMsg:()V
#5 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #39 // JniStudy
#8 = Methodref #7.#31 // JniStudy."":()V
#9 = Methodref #7.#40 // JniStudy.getNativeMsg:()V
#10 = Methodref #7.#41 // JniStudy.printMsg:()V
#11 = String #42 // ####################
#12 = Methodref #7.#43 // JniStudy.nativeCallPrintMsg:()V
#13 = String #44 // /home/myname/spc-work/jni-test/libjnistudy
#14 = Methodref #35.#45 // java/lang/System.load:(Ljava/lang/String;)V
#15 = Class #46 // java/lang/Object
......
通过这个小Demo,JNI最基本的流程就跑通了。在项目学习过程中,可以使用这一方式快速验证,提高效率,避免遇到复杂的工程或环境配置问题。
当然JNI方面还涉及非常多的知识,像C++语言,Android,Linux系统底层API的使用,这就需要结合Framework源码以及NDK来学习了。