文档章节

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

刘小米_思聪
 刘小米_思聪
发布于 2014/09/16 16:27
字数 3728
阅读 4603
收藏 24

                                           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 <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++ 函数实现拥有与头文件中的函数说明相同的说明。

参考文献:

  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 (看)


© 著作权归作者所有

刘小米_思聪
粉丝 58
博文 60
码字总数 43955
作品 0
西安
其他
私信 提问
【jni 编程】—— NDK环境搭建

在《站在巨人的肩膀上,谈app的创新性》一文中 http://my.oschina.net/liusicong/blog/311971,我提到过构建app技术壁垒的必要性。在构建技术壁垒时,我们往往需要调用许多库函数,例如:图像...

刘小米
2014/09/10
758
0
SWIG与JAVA 交互最全开发指南一

项目背景 最近开始研究做移动端项目,但是本人基本是做了五六年的c++的底层研发,对C++的研发可以说是驾轻就熟了,但是对于android还是属于刚入门阶段,虽然断断续续做移动端也做了一年,但是...

揽月凡尘
2018/06/16
0
0
Android JNI(一)——NDK与JNI基础

本系列文章如下: Android JNI(一)——NDK与JNI基础 Android JNI学习(二)——实战JNI之“hello world” Android JNI学习(三)——Java与Native相互调用 Android JNI学习(四)——JNI的常用方法...

隔壁老李头
2018/05/09
0
0
swig3.0 生成不了cpp代码,求高手分析原因..

swig jni自动生成代码配置文件 my-swig-generate.mk # MY_SWIG_PACKAGE MY_SWIG_MODE MY_SWIG_INTERFACES #Android构建系统的SWIG扩展 #@author Onur cinar # #检查变量MY_SWIG_PACKAGE是否已......

2014-jay
2014/04/27
452
0
JNI和NDK的区别

NDK(Native Development Kit)“原生”也就是二进制 android常用的开发方式是java封装的库,而这些库的底层实现是由C/C++实现,如媒体,图形库等 java调用这样实现就需要用JNI(Java Native...

长平狐
2013/01/06
106
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring boot 静态资源访问

0. 两个配置 spring.mvc.static-path-patternspring.resources.static-locations 1. application中需要先行的两个配置项 1.1 spring.mvc.static-path-pattern 这个配置项是告诉springboo......

moon888
今天
3
0
hash slot(虚拟桶)

在分布式集群中,如何保证相同请求落到相同的机器上,并且后面的集群机器可以尽可能的均分请求,并且当扩容或down机的情况下能对原有集群影响最小。 round robin算法:是把数据mod后直接映射...

李朝强
今天
4
0
Kafka 原理和实战

本文首发于 vivo互联网技术 微信公众号 https://mp.weixin.qq.com/s/bV8AhqAjQp4a_iXRfobkCQ 作者简介:郑志彬,毕业于华南理工大学计算机科学与技术(双语班)。先后从事过电子商务、开放平...

vivo互联网技术
今天
19
0
java数据类型

基本类型: 整型:Byte,short,int,long 浮点型:float,double 字符型:char 布尔型:boolean 引用类型: 类类型: 接口类型: 数组类型: Byte 1字节 八位 -128 -------- 127 short 2字节...

audience_1
今天
9
0
太全了|万字详解Docker架构原理、功能及使用

一、简介 1、了解Docker的前生LXC LXC为Linux Container的简写。可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。相当于C++中的NameSpa...

Java技术剑
今天
27
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部