JNI(Java Native Interface)意为JAVA本地调用,它允许Java代码和其他语言写的代码进行交互,简单的说,是一种在Java虚拟机控制下执行代码的标准机制。
我们都知道,Java应用程序是一处编码,处处运行的,之所以可以这么威风,靠的就是JVM这个东西,那么JVM是什么呢,JVM就是Java虚拟机,是一种虚拟技术,位于java应用程序和特定的操作系统之间,担当着“一处编码,处处运行”的重任。他隐藏了操作系统的差异性,使得运行在上层的java程序可以不用管底层到底是什么操作系统。这样,只要你的机子上有JVM,那么你的机子就可以跑java程序,至于你用的是什么操作系统,我不用管。
可是,有的时候,我们并不能抛开特定的操作系统的特性,而是用纯粹的java来完成我们的应用。虽然,java是多么多么的强大,但是它也不是万能的。比如,现在我们的应用需要一些特定操作系统的特性,但是java它不支持,怎么办?或者,我们需要的部分核心功能已经存在了,但是是C/C++的库文件,又怎么办?更常见的一个理由是“这个处理过程性能要求高,java性能太差,满足不了我们的要求,用C/C++实现吧,性能高”!
如此可见,JNI的存在,总有它的理由。但是JNI的使用,我们破坏了java“一处编码,处处运行”的优势,使得我们的应用是“Host-Dependable”。同时,Java是类型安全的,而C/C++不是的,所以,使用JNI,java这两大特性就荡然无存了。因此,要不要使用JNI,我们需要三思而后行。
这里我们通过一个例子来演示JNI的编译流程。例子很简单,就是通过JNI调用实现两数相加,上层传给底层两个int型数字,底层进行相加运算并将结果返回给上层。
1.新建Android工程,申明Native方法
首先新建一个安卓工程,然后.Java中申明Native方法,如果测试自己写着玩放到哪都无所谓,如果有多个native方法需要定义, 不妨写个专用工具类来集中管理。我这里写了一个TestJNI.java工具类, 该工具类声明C库中需要自定义的原生函数,这里提供一个加法运算函数:
public class TestJNI {
public native int add(int x, int y);
}
注意这里的函数申明需要加上Native关键字。
2.编译Native方法工具类
我们这里说的编译Native方法工具类,主要是生成TestJNI.class文件和C库的头文件.h文件。这里需要安装java编译环境jdk和jre,因为我们是使用命令行进行编译,所以还需要添加环境变量。
添加环境变量方法:
java_home 添加jdk的安装目录,注意java_home 要书写正确,安装目录后边不要加分号。
classpath 的对应值是 .;%java_home%\lib\dt.jar;%java_home%\lib\tools.jar 注意这个地方不要漏掉最前面的点 。
在原来的path值后面添加 ;%java_home%\bin;%java_home%\jre\bin
确定后 ,重新启动cmd就可以了,记得要重启哦。
cd到项目的JNI接口类目录,执行命令
将TestJNI.java编译成.class文件,可以看到此目录下会生成TestJNI.class文件
注:在Eclipse中新建类后,Eclipse也会帮助我们在项目下自动生成对应的class文件,位于项目目录下的\bin\classes中。
将生成的.class文件放到\bin\classes目录下。然后cd到项目的根目录,执行命令
将会编译生成.h头文件,可以看到在根目录的jni目录下会生成com_hx_testjni_TestJNI.h文件
头文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_hx_testjni_TestJNI */
#ifndef _Included_com_hx_testjni_TestJNI
#define _Included_com_hx_testjni_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/* * Class: com_hx_testjni_TestJNI * Method: add * Signature: (II)I */
JNIEXPORT jint JNICALL Java_com_hx_testjni_TestJNI_add
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
头文件中定义了与上层的访问接口Java_com_hx_testjni_TestJNI_add,方法名是按照”Java_包名_ 类名 _方法名“来命名的。
可以看到,生成的本地方法中,第一个参数类型是JNIEnv*,第二个参数,如果申明的方法是static类型的,则该参数是一个jclass类型,如果申明的方法是一个非static类型的,则该参数是一个jobject类型。如果你的方法含有参数,那么从第三个参数开始,就是申明时方法的参数了。
3.针对.h编写c/c++逻辑
在jni文件夹下面新建com_hx_testjni_TestJNI.cpp文件,文件代码如下:
#include <stdio.h>
#include "com_hx_testjni_TestJNI.h"
//应用了打印日志头文件
#include <android/log.h>
#define LOG_TAG "HuaXun"
//声明了集中打印方法,包括info,debug,error。其实是调用的android的内部系统文件,在这里对其进行了一个重声明
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, fmt, ##args)
JNIEXPORT jint JNICALL Java_com_hx_testjni_TestJNI_add(JNIEnv *env, jobject obj, jint x, jint y) {
LOGI("(Native)x--->%d", x);
LOGI("(Native)y--->%d", y);
LOGI("(Native)sum--->%d", x+y);
return x + y;
}
注:这里的c/c++文件和.h一样,放到jni目录下即可,名字叫什么无所谓, 网上有人说要和.h文件的文件名一样才行, 其实不用, 叫啥都行,只要Android.mk文件中的LOCAL_SRC_FILES指向该文件就不会出错。
4.编写Android.mk文件
Android.mk文件是在使用NDK编译C代码时必须的文件,Android.mk文件中描述了哪些C文件将被编译且指明了如何编译。掌握Android.mk文件的编写主要是掌握其将要使用的一些关键字,来看一下本例的写法:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := TestJNI
LOCAL_SRC_FILES := com_hx_testjni_TestJNI.cpp
LOCAL_LDLIBS := -lm -llog
include $(BUILD_SHARED_LIBRARY)
系统变量解析:
更多变量参考:http://www.cnblogs.com/hesiming/archive/2011/03/15/1984444.html
5.编译生成.so库
编译C代码到so库需要使用NDK,新版NDK(r7及以上版本)已经集成了Cygwin编译环境。NDK下载地址
下载完成后解压,添加环境变量:
添加变量名NDK_ROOT和变量值D:\android-ndk-r10e.然后在原来的path值后面添加 ;%NDK_ROOT%
cd到jni目录下,在命令行中使用ndk-build编译,过程如下
编译成功后会自动生成libs/armeabi/libTestJNI.so文件。其实就是我们在Android.mk中写的输出模块LOCAL_MODULE,只不过在我们定义的名字前默认添加了lib。
注:Eclipse可以直接关联NDK,使用Eclipse可以更方便地编译so库。首先确认自己的ADT版本,NDK plugin的支持是在ADT 20及以后的版本。
打开Eclipse,Window->Preferences->Android->NDK,设置NDK路径,例如我的是D:\android-ndk-r10e
Eclipse关联ndk-build(自建Builder方法),Project->Properties->Builders->New->Program,新建一个Builder
参数配置:
Main:
Refresh:
Refresh->TestJNI->jni
Build Options:
这样每次我们修改文件后保存或clean后都会重新编译生成.so文件,路径和上面一样。
6.Java中调用.so动态库
安卓项目中使用动态库需要先导入.so库文件,.so库文件存放路径为:根目录/libs/armeabi/,上面步骤中我们已经生成了libTestJNI.so文件在此目录下,可以直接调用。
注意这里生成的是libTestJNI.so, 文件名前面的lib是编译时默认加上去的, 但是在java层loadLibrary时一定不要加lib,否则找不到库文件。
下面来看MainActivity代码吧:
public class MainActivity extends Activity {
//在Java类中的静态代码块中使用System.LoadLibrary()方法加载编译好的 .so动态库
static {
//是TestJNI而不是libTestJNI
System.loadLibrary("TestJNI");
}
EditText tvX = null;
EditText tvY = null;
TextView tvSum = null;
Button btnAdd = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvX = (EditText) findViewById(R.id.et_x);
tvY = (EditText) findViewById(R.id.et_y);
tvSum = (TextView) findViewById(R.id.et_sum);
btnAdd = (Button) findViewById(R.id.btn_add);
btnAdd.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int x = Integer.valueOf(tvX.getText().toString());
int y = Integer.valueOf(tvY.getText().toString());
showlog("(Java)x--->"+x);
showlog("(Java)y--->"+y);
int sum = 0;
//获取JNI接口类对象
TestJNI jni = new TestJNI();
//调用Native方法
sum = jni.add(x, y);
showlog("(Java)sum--->"+sum);
tvSum.setText(String.valueOf(sum));
}
});
}
public void showlog(String info) {
System.out.print("HuaXun " + info + "\n");
}
}
编译成apk文件,安装到手机。其实apk文件安装到手机上之后, .so动态库文件存放在 data/包名/libs 目录下;
运行看看效果吧
可以看到通过JNI调用,上层成功的获取到了Native方法的结果,而所有的运算过程都是底层C++来实现的。
同时看控制台输出的Log:
Demo下载地址