JNI学习(一)之设置编译环境

JNI概述

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的编译流程。例子很简单,就是通过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类型。如果你的方法含有参数,那么从第三个参数开始,就是申明时方法的参数了。

  • JNIEnv参数 : 代表的是Java环境, 通过这个环境可以调用Java里面的方法;
  • jclass参数 : 调用C语言方法的类, this表示当前的类, 即调用JNI方法的类;
  • jobject参数 : 调用C语言方法的对象, this对象表示当前的对象, 即调用JNI方法所在类的对象;

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)

系统变量解析:

  • LOCAL_PATH : 描述所有要编译的C文件所在的根目录,这边的赋值为$(call my-dir),代表根目录即为Android.mk所在的目录。
  • include $(CLEAR_VARS) : 使用NDK编译工具时对编译环境中所用到的全局变量清零,如LOCAL_MODULE,LOCAL_SRC_FILES等,因为在一次NDK编译过程中可能会多次调用Android.mk文件,中间用到的全局变量可能是变化的。
  • LOCAL_MODULE : 最后生成库时的名字的一部分,给其加上前缀lib和后缀.so就是生成的共享库的名字libTestJNI.so。
  • LOCAL_SRC_FILES: 指明要被编译的c文件的文件名。
  • LOCAL_LDLIBS: 编译模块时要使用的附加的链接器选项。这对于使用‘-l’前缀传递指定库的名字是有用的。如-llog表示告诉链接器生成的模块要在加载时刻链接到/system/lib/liblog.so。
  • 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:

  • Name:NDK_Builder
  • Location为ndk-build.cmd的路径,可以如图所示绝对路径,也可以以环境变量的形式,即${NDK_ROOT}\ndk-build.cmd,其中NDK_ROOT为配置的NDK路径
  • Working Directory,为当前的工程下

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下载地址

你可能感兴趣的:(JNI学习(一)之设置编译环境)