JNI入门教程:最小环境HelloWorld实战

JNI是Android应用开发中不太常涉及的技术,但在Framework层中却被广泛使用。作为一名Android应用开发人员,学习JNI知识,对理解整个系统原理还是有很大帮助的。

学习JNI有很多途径:

  1. 可以直接阅读Framework源码。这种方案不太好上手验证,因为Framework代码要配置的编译环境还是比较复杂的,而且编译后没法直接运行测试,需要Root系统
  2. 其次也可以下载安装NDK,直接在Android Studio里开发项目。这种方式也有比较多的环境配置工作,并且操作起来比较麻烦。

因此这篇文章探讨的主题就是在最小环境下,进行JNI的学习与验证。JNI是Java本身就具备的能力,所以最小环境就是Java运行的环境,不需要任何Android开发工具以及IDE
为了方便调试与编译,这里我使用Ubuntu系统,需要安装的开发工具有:

  • openjdk:这里采用的是openjdk11版本
  • gcc:通过apt-get install build-essential安装
  • 任意一个文本编辑器

JNI Hello World

在这个JNI例子中,主要做这几件事:

  1. Java中调用JNI函数
  2. JNI中打印Java中的变量
  3. 在JNI中修改Java中变量
  4. 在JNI中调用Java方法

编写Java代码

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代码

然后在同一个目录下,创建一个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方法。
详细的过程可参考代码注释。

编译C代码

编写好native层逻辑,就可以编译C代码了。编译需要两个头文件jni.hjni_md.h。在头文件JniStudy.h中,可以看到自动生成代码已经引用了jni.hjni_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的库文件。

载入JNI Lib

再次更改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 两条链路都打通了。

一些问题

以下是我在调试中遇到的一些问题,有的在上面已经提到过,在这里再次汇总

1.提示javah命令找不到

这可能和JDK版本有关。旧版本JDK可以使用javah命令。新版中(可能是JDK10以上)javac命令已经集成了生成头文件功能。

2.gcc编译报错

/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
3.运行时报错Fatel Error,崩溃退出

错误信息

A fatal error has been detected by the Java Runtime Environment

native层方法SetXXXField,GetXXXField等的传参不能为空,当代码编写错误,传入参数为空时就会崩溃。可以通过调试或打印信息,确定传参出错位置。

4.native层方法文档在哪里?

native层方法例如FindClass,GetFieldID,NewStringUTF等等,其实都是有官方文档的:
JNI官方完整文档:Java Native Interface Specification Contents
JNI方法列表及文档:Chapter 4: JNI Functions

5.native中的Java类型签名怎么确定

官网文档: 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来学习了。

你可能感兴趣的:(Android,工具配置,java)