android开发教程(3)— jni编程之采用SWIG从Java调用C/C++

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

                                           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 方式

  1. 在jni文件夹下编写C/C++代码,实现我们想要实现的C/C++逻辑。

  2. 根据C/C++代码,编写 Java 代码。我们将根据写好的C/C++函数,编写 Java 类,这些类执行三个任务:声明将要调用的native本机方法;装入包含本机代码的共享库;然后调用该本机方法。

  3. 首先用javah生成C/C++ 头文件(.h 文件),然后去改写这个头文件的方法,将我们自己的东西添加进去。C/C++的头文件将声明想要调用的本机函数说明。然后,这个头文件与 C/C++ 函数实现(请参阅步骤 4)一起来创建共享库(请参阅步骤 5)。

  4. 写一个Android.mk文件,放在jni下的C/C++代码文件夹下

  5. 编译运行 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  
#include 
#include 
#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 
    #include  
// 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  
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++ 函数实现拥有与头文件中的函数说明相同的说明。

参考文献:

  1. http://www.cnblogs.com/BloodAndBone/archive/2010/12/22/1913882.html

  2. SWIG官网的例子:http://www.swig.org/Doc2.0/Android.html#Android_examples_intro

  3. Andriod.mk详解: http://www.2cto.com/kf/201310/253386.html

  4. http://bbs.51cto.com/thread-948244-1.html (看)


转载于:https://my.oschina.net/liusicong/blog/314162

你可能感兴趣的:(android开发教程(3)— jni编程之采用SWIG从Java调用C/C++)