JNI是Java Native Interface的缩写,中文为JAVA本地调用。使用JNI可以很方便的用我们的Java程序调用C/C++程序。很多时候,某些功能用Java无法实现,比如说涉及到底层驱动的一些功能,这时候我们就可以利用JNI来调用C或者C++程序来实现,这就是JNI的强大之处。但是JNI也有它的缺点,使用java与本地已编译的代码交互,通常会丧失平台可移植性。
在Android中调用C/C++程序也是一样的,基本和Java调用的方式一致,只是Android使用NDK编译出动态链接库,供Androd程序调用。本文以一个简单的Android JNI调用程序来讲解整个过程。
NDK是Google提供了交叉编译工具链, 能够在linux/Windows平台编译出在arm平台下执行的二进制库文件.
编写Java类
创建Java类,在里面定义一个本地方法(用native关键字修饰的方法);
1 | package com.example.androidjnitest; |
使用javah生成头文件
以eclipse为例,加入到项目的src
目录,运行javah com.example.androidjnitest.Hello
,如果目录不正确,可能会出现找不到类文件的情况。正常情况会生成下面的com_example_androidjnitest_Hello.h
头文件,具体文件名与包名相关。文件的内容如下:
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
函数声明的形式为Java_完整类名_方法名
, 完整类名包括了包名。
注意:
- 此头文件不需要用户编辑,是直接供其它C、C++程序引用的。
- 此头文件中的函数方法名,是将来与动态链接库交互的接口,并需要名字保持一致。
在注释中我们可以看到 Signature, 这个是方法的签名,用于指明函数的返回类型。关于不同的Signature, 见下表:
实现头文件中的函数
下面以简单的C程序为例来说明,C++在转换字符串等操作时略有不同。本例中C/C++编写都没有区别。
在实现对应
.h
文件时,可以在当前项目里面新建一个jni
文件夹,将之前的.h
文件和将要实现的.c
文件放在该文件夹中。
1 |
|
上面是一个很简单的例子,如果工程比较大,可能需要在.c
文件中包含很多其他C的头文件,并在每个函数实现中做一些类型转换之类的。
JNIEnv结构体系 : JNIEnv指针指向一个线程相关的结构,线程相关结构指向一个指针数组,指针数组中的每个元素最终指向一个JNI函数。jni.h
中声明了JNIEnv : C语言中定义的JNIEnv 是 JNINativeInterface* , C++中定义的JNIEnv 是 _JNIEnv。
1 | struct _JNIEnv; |
关于JNIEnv指针调用解析 :
- C中
JNIEnv
就是const struct JNINativeInterface*
,JNIEnv * env
等价于JNINativeInterface** env
, 因此要得到JNINativeInterface结构体中定义的函数指针, 就必须先获取到 JNINativeInterface的一级指针对象 即*env
, 该一级指针对象就是JNINativeInterface* env
, 然后通过该一级指针对象调用JNI函数 :(*env)->NewStringUTF(env, "hello")
; - C++ 中的
JNIEnv
就是_JNIEnv
结构体, 二者是等同的; 因此在调用 JNI函数的时候, 只需要使用env->NewStringUTF(env, "hello")
方法即可, 不用在进行*
运算;
在JNINativeInterface结构体中定义了一系列的关于Java操作的相关方法 :
1 | /* |
更多的函数可以参考官方文档:JNI Functions
NDK编译生成so库
在jni
文件夹下建立Android.mk
文件,用于编译生成库文件。
1 | # Copyright (C) 2009 The Android Open Source Project |
makefile中变量解释:
LOCAL_PATH := $(call my-dir)
一个 Android.mk 文件首先必须定义好 LOCAL_PATH 变量。它用于在开发树中查找源文件。在这个例子中,宏函数’my-dir’, 由编译系统提供,用于返回当前路径(即包含 Android.mk file 文件的目录)。include $( CLEAR_VARS)
CLEAR_VARS 由编译系统提供,指定让 GNU MAKEFILE 为你清除许多 LOCAL_XXX 变量(例如 LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES, 等等…), 除 LOCAL_PATH 。这是必要的,因为所有的编译控制文件都在同一个 GNU MAKE 执行环境中,所有的变量都是全局的。LOCAL_MODULE := Hello
编译的目标对象,LOCAL_MODULE 变量必须定义,以标识你在Android.mk文件中描述的每个模块。名称必须是唯一的,而且不包含任何空格。编译系统会自动产生合适的前缀和后缀,换句话说,一个被命名为hello的共享库模块,将会生成libhello.so文件。LOCAL_SRC_FILES := com_example_androidjnitest_Hello.c
LOCAL_SRC_FILES 变量必须包含将要编译打包进模块中的 C 或 C++ 源代码文件。注意,你不用在这里列出头文件和包含文件,因为编译系统将会自动为你找出依赖型的文件;仅仅列出直接传递给编译器的源代码文件就好。include $(BUILD_SHARED_LIBRARY)
BUILD_SHARED_LIBRARY 表示编译生成共享库,是编译系统提供的变量,指向一个 GNU Makefile 脚本,负责收集自从上次调用include $(CLEAR_VARS)
以来,定义在 LOCAL_XXX 变量中的所有信息,并且决定编译什么,如何正确地去做。还有BUILD_STATIC_LIBRARY
变量表示生成静态库:lib$(LOCAL_MODULE).a
,BUILD_EXECUTABLE
表示生成可执行文件。
接着在makefile所在目录,使用ndk-build
编译生成so
库,如果没有问题,可以看到so
库存放的位置,如果有语法错误,进行相应的修改。
至此生成so
的步骤完成,接下来可以在程序中测试so
是否运行正常。下面的Androd程序在主Activity中为button(button需要先添加到layout中)绑定了一个监听器,用于调用Hello
类中的JNI方法,点击button出现4就说明调用成功。
1 | private Button button; |
参考链接: