Android 从Java调用C/C++html
当没法用 Java 语言编写整个应用程序时,JNI 容许您调用C/C++本机代码。在下列典型状况下,您可能决定使用本机代码:前端
但愿用更低级、更快的编程语言C/C++去实现对时间有严格要求的代码。java
但愿从 Java 程序访问旧代码或代码库。android
须要标准 Java 类库中不支持的依赖于平台的特性。web
我在安卓项目中,须要用到C++的soundtouch库函数,所以必须将调用该库的代码用C++编写,而后再由java调用C++本机代码。shell
前提:已经配置好支持交叉调用的NDK(Native Development Kit,java与C/C++交叉调用的工具),并为你的工程建立好builder,配置可参照个人另外一篇博文:http://my.oschina.net/liusicong/blog/311886。编程
网上有不少jni教程,可是对于安卓开发爱好者,如何在java代码中调用C/C++函数,实现咱们想要的功能,却没有一个十分合适的教程,所以我写下本文。数组
我要解决的问题:安卓前端有一个按钮,点击该按钮就能够实现“声音特效处理”的功能。而这个功能的后台实现的主要逻辑由C/C++代码编写,所以须要从java调用C/C++代码。app
我看了网上的关于 jni编程 的教程不少,但不尽相同,刚开始会犯迷糊。我想笔者每每忽略了一个关键点,那就是采用了什么方式决定了步骤的流程。有两种生成 jni的方式:一种是经过SWIG从C++代码生成过分的java代码;另外一种是经过javah的方式从java代码自动生成过分的C++代码。两种方式下的步骤流程正好相反。eclipse
第一种方式:因为须要配置SWIG环境,有点麻烦了,因此每每你们不采用这个途径(本文将介绍的步骤就是这种状况),官方文档的例子值得一看:http://www.swig.org/Doc2.0/Android.html#Android_examples_intro。(我抽空把这个官方文档可翻译下)
第二种方式:javah的方式则经过shell指令就能够完成整个流程,因此网上的教程也多数是这一类的,可参照个人另外一篇博文http://my.oschina.net/liusicong/blog/315826。
安卓开发中,从 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 包
——————————————————————————————
咱们首先编写一个.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 ) { /*具体实现*/ }
根据编写好的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++中类的方法了。
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 伪类是如何相关的。
理论上来讲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 文件。
最后一步是运行 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++ 函数实现拥有与头文件中的函数说明相同的说明。
参考文献: