Android 从Java调用C/C++
当无法用 Java 语言编写整个应用程序时,JNI 允许您调用C/C++本机代码。在下列典型情况下,您可能决定使用本机代码:
希望用更低级、更快的编程语言C/C++去实现对时间有严格要求的代码。
希望从 Java 程序访问旧代码或代码库。
需要标准 Java 类库中不支持的依赖于平台的特性。
我为什么需要它?我的代码背景
我在安卓项目中,需要用到C++的soundtouch库函数,因此必须将调用该库的代码用C++编写,然后再由java调用C++本机代码。
前提:已经配置好支持交叉调用的NDK(Native Development Kit,java与C/C++交叉调用的工具),并为你的工程创建好builder,配置可参照我的另一篇博文:http://my.oschina.net/liusicong/blog/311886。
问题及动机
网上有很多jni教程,但是对于安卓开发爱好者,如何在java代码中调用C/C++函数,实现我们想要的功能,却没有一个十分合适的教程,因此我写下本文。
我要解决的问题:安卓前端有一个按钮,点击该按钮就可以实现“声音特效处理”的功能。而这个功能的后台实现的主要逻辑由C/C++代码编写,因此需要从java调用C/C++代码。
须知:SWIG和javah的区别(强烈推荐)
我看了网上的关于 jni编程 的教程很多,但不尽相同,刚开始会犯迷糊。我想笔者往往忽略了一个关键点,那就是采用了什么方式决定了步骤的流程。有两种生成 jni的方式:一种是通过SWIG从C++代码生成过度的java代码;另一种是通过javah的方式从java代码自动生成过度的C++代码。两种方式下的步骤流程正好相反。
第一种方式:由于需要配置SWIG环境,有点麻烦了,所以往往大家不采用这个途径(本文将介绍的步骤就是这种情况),官方文档的例子值得一看:http://www.swig.org/Doc2.0/Android.html#Android_examples_intro。(我抽空把这个官方文档可翻译下)
第二种方式:javah的方式则通过shell指令就可以完成整个流程,所以网上的教程也多数是这一类的,可参照我的另一篇博文http://my.oschina.net/liusicong/blog/315826。
解决方案:从 Java 代码调用 C/C++ 的五个步骤
安卓开发中,从 Java 程序调用 C 或 C ++ 代码的过程由五个步骤组成。我们将在深入讨论每个步骤,首先迅速地浏览一下,注意本文采用的方式是:SWIG 方式。
在jni文件夹下编写C/C++代码,实现我们想要实现的C/C++逻辑。
根据C/C++代码,编写 Java 代码。我们将根据写好的C/C++函数,编写 Java 类,这些类执行三个任务:声明将要调用的native本机方法;装入包含本机代码的共享库;然后调用该本机方法。
首先用javah生成C/C++ 头文件(.h 文件),然后去改写这个头文件的方法,将我们自己的东西添加进去。C/C++的头文件将声明想要调用的本机函数说明。然后,这个头文件与 C/C++ 函数实现(请参阅步骤 4)一起来创建共享库(请参阅步骤 5)。
写一个Android.mk文件,放在jni下的C/C++代码文件夹下
编译运行 Java 程序。运行该代码,并查看它是否有用。我们还将讨论一些用于解决常见错误的技巧。
相关代码目录结构(以我的代码结构为例)
src(放java代码)
|_ org.tecunhuman. jni 包(自定义命名的包)
|_ wrapperJNI.java (自己编写的java代码,含native方法)
jni (放C/C++代码)
|_ soundstrech包(我的C++代码)
|_ gen包
|_ wrapper_wrap.cpp
|_ Android.mk
|_ RunParameters.cpp
|_ RunParameters.h
|_ SoundStrech.cpp
|_ SoundStrech.h
|_ WavFile.cpp
|_ WavFile.h
|_ wrapper.i
|_ soundtouch 包
——————————————————————————————
步骤 1:编写C/C++代码(.cpp文件)放在下jni下的C/C++代码文件夹
我们首先编写一个.cpp文件,
//SoundStrech.cpp代码
#include <stdexcept>
#include <stdio.h>
#include <string.h>
#include "RunParameters.h"
#include "WavFile.h"
#include "SoundTouch.h"
#include "BPMDetect.h"
#include "SoundStretch.h"
using namespace soundtouch;
using namespace std;
// Processing chunk size
#define BUFF_SIZE 2048
#if WIN32
#include <io.h>
#include <fcntl.h>
// Macro for Win32 standard input/output stream support: Sets a file stream into binary mode
#define SET_STREAM_TO_BIN_MODE(f) (_setmode(_fileno(f), _O_BINARY))
#else
// Not needed for GNU environment...
#define SET_STREAM_TO_BIN_MODE(f) {}
#endif
static void openFiles(WavInFile **inFile, WavOutFile **outFile, const RunParameters *params)
{
/*省略 具体实现*/
}
// command line parameters
static void setup(SoundTouch *pSoundTouch, const WavInFile *inFile, const RunParameters *params)
{
/*具体实现*/
}
int run(RunParameters *params)
{
/*具体实现*/
}
SoundStretch::~SoundStretch() {
}
void SoundStretch::process(
std::string inFileName,
std::string outFileName,
float tempoDelta,
float pitchDelta,
float rateDelta
) {
/*具体实现*/
}
步骤 2:根据C/C++代码,编写 Java 代码
根据编写好的C/C++函数来写java代码,怎么理解这句话呢?
假设我们先回到纯粹的C++代码编写情形中,您可能会写很多C++的函数,大多数是一系列的中间逻辑(如A调用B,B调用C等),但只有一个入口函数放在启动函数 — Main()函数中被执行调用,来实现我们的某个功能。一个比喻:就像是一串珠子,总有一个线头可以被人捏着拎起来。
那么在我们java与C++交叉调用的情形下,步骤2— 根据C/C++代码,编写Java 代码,java代码中的native方法就像是那个在main函数中被调用的方法,所以应该是根据C++代码中的具体逻辑决定的。
我们从编写 Java 源代码文件开始,它将声明本机方法(或方法),装入包含本机代码的共享库,然后实际调用本机方法。
//wrapperJNI.java代码
package org.tecunhuman.jni;
class wrapperJNI {
//声明native方法,不能实现它(类似抽象方法,但用途不同)
public final static native long new_SoundStretch();
public final static native void delete_SoundStretch(long jarg1);
//调用步骤一的C++代码中的SoundStretch类的SoundStretch::process方法
public final static native void SoundStretch_process(long jarg1, SoundStretch jarg1_, String jarg2, String jarg3, float jarg4, float jarg5, float jarg6);
}
这段代码做了些什么?
首先,请注意对 native 关键字的使用,它只能随 方法 一起使用。native 关键字告诉 Java 编译器:该方法是用 Java 类之外的本机代码实现的,但其声明却在 Java 中。只能在 Java 类中声明 本机方法,而不能实现它(但是不能声明为抽象的方法,使用native关键字即可),所以java文件中的native本机方法不能拥有方法主体。
当然还需要编写几个其他的java文件,去调用wrapperJNI 的 SoundStretch_process成员方法,实现我在java中真正要做的事。但这不是本文想要讨论的重点(这是跟你要实现的业务逻辑有关的,如何设计就是读者的事了)。
简而言之,由于跨语言,java不能直接调用C++函数,而java文件夹下的native方法就像是给C++函数换了个皮,加了个native在此申明下,java就可以调用C++中类的方法了。
步骤 3:通过javah命令,生成C/C++ 头文件
C/C++ 头文件,定义本机函数说明。完成这一步的方法之一是使用 javah.exe,它是随 SDK 一起提供的本机方法 C++ 存根生成器工具。这个工具被设计成用来创建头文件,该头文件为在 Java 源代码文件中所找到的每个 native 方法定义 C++ 风格的函数。
javah 怎么用?
为了便于理解,这里举个栗子:使用eclipse建立一个工程假设工程路径为$ProjectPath,并且你已经定义了一个HelloJni.java类,带有包名cn.com.comit.jni。
package cn.com.comit.jni;
public class HelloJni{
public native void displayHelloJni();
}
那么这时eclipse会自动帮你编译出一个字节码文件HelloJni.class,路径是$ProjectPath\bin\cn\com\comit\jni。
切记cd到包的上一级目录(我们这里是$ProjectPath\bin)即可,写错便会出错。执行以下操作语句就搞掂了。生成的 .h头文件,记得放进你在eclipse工程的 jni 文件夹下,就结束了。
cd ProjectPath\bin
javah -classpath.cn.com.comit.jni.HelloJni
头文件 SoundStrech.h 长什么样子?
// SoundStrech.h
#ifndef SOUNDSTRETCH_H
#define SOUNDSTRETCH_H
#include <string>
class SoundStretch {
public:
SoundStretch();
~SoundStretch();
void process(
std::string inFilename,
std::string outFilename,
float tempoDelta,
float pitchDelta,
float rateDelta
);
};
#endif
关于 C/C++ 头文件
正如您可能已经注意到的那样,SoundStrech.h 中的 C/C++ 函数说明和wrapperJNI.java中的 Java native 方法声明有很大差异。JNIEXPORT 和 JNICALL 是用于导出函数的、依赖于编译器的指示符。返回类型是映射到 Java 类型的 C/C++ 类型。附录 A:JNI 类型中完整地说明了这些类型。
除了 Java 声明中的一般参数以外,所有这些函数的参数表中都有一个指向 JNIEnv 和 jobject 的指针。指向 JNIEnv 的指针实际上是一个指向函数指针表的指针。正如将要在步骤 4 中看到的,这些函数提供各种用来在 C 和 C++ 中操作 Java 数据的能力。
jobject 参数引用当前对象。因此,如果 C 或 C++ 代码需要引用 Java 函数,则这个 jobject 充当引用或指针,返回调用的 Java 对象。函数名本身是由前缀“Java_”加全限定类名,再加下划线和方法名构成的。
JNI类型
JNI 使用几种映射到 Java 类型的本机定义的 C 类型。这些类型可以分成两类:原始类型和伪类(pseudo-classes)。在 C 中,伪类作为结构实现,而在 C++ 中它们是真正的类。
Java 原始类型直接映射到 C 依赖于平台的类型,如下所示:
C 类型 jarray 表示通用数组。在 C 中,所有的数组类型实际上只是 jobject 的同义类型。但是,在 C++ 中,所有的数组类型都继承了 jarray,jarray 又依次继承了 jobject。下列表显示了 Java 数组类型是如何映射到 JNI C 数组类型的。
这里是一棵对象树,它显示了 JNI 伪类是如何相关的。
步骤 4:写一个Android.mk文件,放在jni下的C/C++代码文件夹下
理论上来说java和C++两种语言,需要两种编译环境。NDK,是用于jni本地源码编译的工具,为开发人员将本地代码集成在android代码中提供了方便。实际上NDK和完整源码编译环境一样,都使用安卓的编译系统 —— 通过Android.mk文件控制编译。因此在编译前必须书写好Android.mk文件。
编写Android.mk时,必须要写的5句话:
Local_PATH:=$(call.my-dir)//必须位于文件最开始。用来定位源文件位置,$(call my-dir)返回当前目录的路径
include $(CLEAR_VARS)
Local_MODEL:= libsoundtouch //此句指定.so文件的名称
LOCAL_SRC_FILES := \
RunParameters.cpp \
WavFile.cpp \
SoundStretch.cpp \
gen/wrapper_wrap.cpp //指定C++源文件路径,多个源文件用"\"分开
include $(BUILD_SHARED_LIBRARY)//最后加编译
更多Android.mk书写细节可查看:http://www.2cto.com/kf/201310/253386.html
还可以有一个Application.mk应该和Andoird.mk并列放在一个目录下,但不是必须的。
注意,Android.mk文件必须编写正确。这样一来NDK编译完成后则会将生成的.so文件放在正确的位置(libs/armbi目录下)。
解释:
(1)CLEAR_VARS 由编译系统提供(可以在 android 安装目录下的/build/core/config.mk 文件看到其定义,为 CLEAR_VARS:=$(BUILD_SYSTEM)/clear_vars.mk),指定让GNU MAKEFILE该脚本为你清除许多 LOCAL_XXX 变量 ( 例如 LOCAL_MODULE , LOCAL_SRC_FILES ,LOCAL_STATIC_LIBRARIES,等等…),除 LOCAL_PATH。这是必要的,因为所有的编译文件都在同一个 GNU MAKE 执行环境中,所有的变量都是全局的。所以我们需要先清空这些变量(LOCAL_PATH除外)。又因为LOCAL_PATH总是要求在每个模块中都要进行设置,所以并需要清空它。
(2)LOCAL_MODULE 变量必须定义,以标识你在 Android.mk 文件中描述的每个模块。名称必须是唯一的,而且不包含任何空格。注意编译系统会自动产生合适的前缀和后缀,换句话说,一个被命名为'foo'的共享库模块,将会生成'libsoundtouch.so'文件。注意:如果把库命名为‘libsoundtouch‘,编译系统将不会添加任何的 lib 前缀,也会生成 libsoundtouch.so。
(3)LOCAL_SRC_FILES 变量必须包含将要编译打包进模块中的 C 或 C++源代码文件。不用
在这里列出头文件和包含文件,编译系统将会自动找出依赖型的文件,当然对于包含文件,你包含时指定的路径应该正确。
(4)BUILD_SHARED_LIBRARY 是编译系统提供的变量,指向一个 GNU Makefile 脚本(应该
就是在 build/core 目录下的 shared_library.mk) ,将根据LOCAL_XXX系列变量中的值,来编译生成共享库(动态链接库)。如果想生成静态库,则用BUILD_STATIC_LIBRARY在NDK的sources/samples目录下有更复杂一点的例子,写有注释的 Android.mk 文件。
步骤 5:编译程序
最后一步是运行 Java 程序,并确保代码正确工作。因为必须在 Java 虚拟机中执行所有 Java 代码,所以需要使用 Java 运行时环境。完成这一步的方法之一是使用 java,它是随 SDK 一起提供的 Java 解释器。所使用的命令是:
java -cp . test.Sample1
输出:
intMethod: 25
booleanMethod: false
stringMethod: JAVA
intArrayMethod: 33
故障排除
当使用 JNI 从 Java 程序访问本机代码时,您会遇到许多问题。您会遇到的三个最常见的错误是:
无法找到动态链接。它所产生的错误消息是:java.lang.UnsatisfiedLinkError。这通常指无法找到共享库,或者无法找到共享库内特定的本机方法。
无法找到共享库文件。当用 System.loadLibrary(String libname) 方法(参数是文件名)装入库文件时,请确保文件名拼写正确以及没有指定扩展名。还有,确保库文件的位置在类路径中,从而确保 JVM 可以访问该库文件。
无法找到具有指定说明的方法。确保您的 C/C++ 函数实现拥有与头文件中的函数说明相同的说明。
参考文献:
http://www.cnblogs.com/BloodAndBone/archive/2010/12/22/1913882.html
SWIG官网的例子:http://www.swig.org/Doc2.0/Android.html#Android_examples_intro
Andriod.mk详解: http://www.2cto.com/kf/201310/253386.html