入门 JNI (Android NDK 教程 二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Lee_Swifter/article/details/88372610

第一讲用一些概念来理解来原生开发能做什么,有哪些功能。那么从现在开始,就开始真正进入编码阶段了,作为 Android NDK 开发,最先需要知道的,就是 JNI 了。它是一座桥梁,是 Java 代码与 C 代码之间的桥梁,是 Android 开发通往原生开发的必要路径。因此,这一讲的主要内容就对 JNI 有一个初步的认识。

初识 JNI

JNI 全称是 Java Native Interface(Java 本地接口)单词首字母的缩写,本地接口就是指用 C 和 C++ 开发的 接口。由于 JNI 是 JVM 规范中的一部份,因此可以将我们写的 JNI 程序在任何实现了 JNI 规范的 Java 虚拟机 中运行。同时,这个特性使我们可以复用以前用 C/C++ 写的大量代码。

开发 JNI 程序会受到系统环境的限制,因为用 C/C++ 语言写出来的代码或模块,编译过程当中要依赖当前操作 系统环境所提供的一些库函数,并和本地库链接在一起。而且编译后生成的二进制代码只能在本地操作系统环境 下运行,因为不同的操作系统环境,有自己的本地库和 CPU 指令集,而且各个平台对标准 C/C++ 的规范和标准 库函数实现方式也有所区别。这就造成使用了 JNI 接口的 JAVA 程序,不再像以前那样自由的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相应的动态库。

使用 JNI

此教程虽然是 Android NDK 开发教程,但是在本节,我们暂时脱离 Android 开发环境。因为 JNI 是 Java 提供的特性,我们完全可以仅在 Java 环境中使用 JNI。当然使用 Android Studio 肯定是没问题的,只是当你在最简单的开发环境下使用了 JNI,你才能对 JNI 这个 Java 特性有个更加独立的了解。

JNI 开发流程主要分为以下 6 步:

  1. 编写声明了 native 方法的 Java 类
  2. 将 Java 源代码编译成 class 字节码文件
  3. 用 javah -jni 命令生成 .h 头文件
  4. 用本地代码实现 .h头 文件中的函数
  5. 将本地代码编译成动态库(Windows:*.dll,linux/unix:*.so,mac os x:*.jnilib)
  6. 拷贝动态库至 java.library.path 本地库搜索目录下,并运行 Java 程序

接下来,我们就一步一步地实现以上的步骤。

第一步,编写 Java 代码

首先,我在任意一个文件夹下创建一个简单的 Java 类,如下:

public class HelloJNI {
  
        public static void main(String[] args) {
                String theStringFromNative = getString("hello jni");
                System.out.println(theStringFromNative);
        }

        public static native String getString(String str);
}

注意在代码中有一个名为getString() 的 native 函数。我们将在 C 代码中为其提供实现。

第二步,编译生成 class 文件

这一步就很容易了,调用 javac 没有错误就会生成对应的 class 文件。这里就不赘述了。
在这里,假如你不进行后面的操作就运行 class 文件,你就会得到如下的错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloJNI.getString(Ljava/lang/String;)Ljava/lang/String;
	at HelloJNI.getString(Native Method)
	at HelloJNI.main(HelloJNI.java:4)

这里的内容就是找不到原生方法,并给出了原生方法的签名。

现在,你的文件夹里应该有两个文件了,一个是 java 文件,一个是 class 文件。下面就来看看如何生成 h 文件。

第三步,使用 javah 生成头文件

在JDK 中,自带的这个 javah 命令行工具可以为原生方法解析 Java 类文件并生成由原生方法声明组成的头文件。现在我们就来调用这个命令:

javah HelloJNI

就是这么简单。现在你的文件夹中应该有了 h 文件。关于 javah 命令,其还有很多选项,可以通过 -h 来查看,这里就不做讲解了。来看一看 h 文件里面有什么内容:

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

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    getString
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_HelloJNI_getString
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

内容有很多,对于大多数内容,我们后面再用一期来讲解,现在我们只需要知道这里面有一个 C 语言的函数声明:jstring JNICALL Java_HelloJNI_getString (JNIEnv *, jclass, jstring);

作为一个函数的声明,它是不包括函数体的,下面我们将为这个函数编写一个简单的实现。

第四步,实现原生函数

现在我们来编写一个简单的 C 语言函数实现,如下:

#include "HelloJNI.h"

JNIEXPORT jstring JNICALL Java_HelloJNI_getString(JNIEnv *, jclass, jstring) {
    return (*env)->NewStringUTF(env, "Hello from JNI !");
}

其内容就是返回一个字符串:Hello from JNI !。接下来我们来把这个文件编译成本地动态库文件。

第五步,编译动态库

将 C/C++ 代码编译成本地动态库文件动态库文件名命名规则:lib+动态库文件名+后缀(操作系统不一 样,后缀名也不一样):

  • Mac OS X :libHelloJNI.jnilib
  • Windows :HelloJNI.dll(注意,不需要 lib 前缀)
  • Linux/Unix :libHelloJNI.so

每个平台的编译方式也不一样,因为我的电脑是 Mac 的,所以就说一说在 Mac 如何编译动态库。

gcc -dynamiclib -o libHelloJNI.jnilib HelloJNI.c  -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin

这里的 JAVA_HOME 是需要配置到环境变量中去的,其他的函数说明:

  • -dynamiclib :表示编译成动态链接库
  • -o:指定动态链接库编译后生成的路径及文件名
  • -I:指定相关头文件

这样下来就会有一个文件libHelloJNI.jnilib,这个文件就是我们需要的动态链接文件。

在这一步里可能会出现很多坑,遇到问题得一个一个解决,大部分都是由于 gcc 的参数配置的问题造成的,这个时候就需要多查一查,多多了解 gcc 这个工具。

配置链接库,运行 Java 程序

在 Java 程序中,调用 native 方法之前,需要先加载动态链接库。否则就会抛出第二步中的找不到链接库的错误。

一般在类的静态(static)代码块中加载动态库最合适,因为在创建类的实例时,类会被 ClassLoader 先加载到虚拟机,随后立马调用类的 static 静态代码块。这时再去调用 native 方法就万无一失了。加载动态库的两种方式:

  1. System.loadLibrary("HelloJNI");
    只需要指定动态库的名字即可,不需要加 lib 前缀,也不要加 .so 、.dll 和 .jnilib 后缀。
    使用此方式时 Java 会去 java.library.path 系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError 异常。而且,在查找时,系统会根据不同系统查找不同名称的文件,如果在 Mac 下找 libHelloJNI.jnilib 文件,linux 下找 libHelloJNI.so 文件,Windows 下找 libHelloJNI.dll 文件。
  2. System.load("/Users/avril/jni/libHelloJNI.jnilib");
    指定动态库的绝对路径名,需要加上前缀和后缀

所以综上所属,有两种方法能够让 Java 程序找到动态库:

  • 将动态链接库拷贝到java.library.path目录下
  • 给 jvm 添加“-Djava.library.path=动态链接库搜索目录”参数

为了简单起见,我将 libHelloJNI.jnilib 文件放到与 HelloJNI.class 文件相同文件夹下,然后在运行 class 文件。不过在运行 class 文件之前,我们需要对 java 代码更改以下,因为我们的代码中还没有加载链接库,所以我在代码中加入如下代码:

static {
		//此处也可以使用 load 方法并使用链接库的绝对地址
		System.loadLibrary("HelloJNI");
	}

下面我们再编译运行 class 文件,那么将得到如下输出:

Hello from JNI !

而这个字符串就是从 C 语言中返回的。这就是从 Java 使用 JNI 调用 native 方法的步骤。

总结

使用 JNI 的基本步骤就是如上所示,但是 JNI 还有非常非常多的内容,这是一篇文章是无法囊括的,比如说 native 函数调用 Java 代码,已经两种语言数据交互等,这些内容我将后面分篇讲解。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Lee_Swifter/article/details/88372610