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

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

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

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

  1. android开发教程,android开发入门教程

    所谓知己知彼才能百战百胜,想学好android就必须先了解 android是什么意思 android环境搭建 Android一词的本义指"机器人",同时也是Google于2007年 ...

  2. Android开发中的多线程编程技术

    Android开发中的多线程编程技术 [IT168技术]多线程这个令人生畏的"洪水猛兽",很多人谈起多线程都心存畏惧.在Android开发过程中,多线程真的很难吗?多线程程序的&q ...

  3. ldflags android,Android 开发手记一 NDK编程实例

    Android开发手记一 ---- NDK编程实例 在Android上,应用程序的开发,大部分基于Java语言来实现.要使用c或是c++的程序或库,就需要使用NDK来实现.NDK是Native Dev ...

  4. android开发教程(一)——目录

    本教程中使用的环境: windows平台: c:\>winver linux平台: k@k-C410:/$ lsb_release -a No LSB modules are available ...

  5. Android开发教程JAVA基础(汇总)

    Android游戏开发视频教程(汇总) Android开发教程JAVA基础之Java 概述 Android开发教程JAVA基础之标示符.关键字1 Android开发教程JAVA基础之标示符.关键字2 ...

  6. 做了一个系列的Android开发教程列表

    做了一个系列的Android开发教程列表.花了半天多的专题 里面包含了 4个系列的教程. 也包含了很多Android开发资料. 喜欢的人可以收藏哦:http://dev.apkbus.com/

  7. 【Android开发教程】一、基础概念

    Android操作系统 Android是一个基于Linux.使用java作为程序接口的操作系统.他提供了一些工具,比如编译器.调试器.还有他自己的仿真器(DVM - Dalvik Virtual Ma ...

  8. Android开发教程之--sql语句一、创建/删除表Stringsql=Createtable

    Android开发教程之--sql语句 一.创建/删除表 String sql="Create table "+TABLE_NAME+"("+FIELD_ID+ ...

  9. “.NET研究”【Android开发教程】一、基础概念

    Android操作系统 Android是一个基于Linux.使用java作为程序接口的操作系统.他提供了一些工具,比如编译器.调试器.还有他自己的仿真器(DVM - Dalvik Virtual Ma ...

最新文章

  1. R语言连接MySQL报错:could not run statement: The used command is not allowed with this MySQL version
  2. mysql取n条不重复_MySQL重复数据中限定操作n条
  3. 计算机应用基础本科常见问题讨论,《计算机应用基础》(本科)2017年6月期末考试指导.pdf...
  4. 【nRF51822学习教程】SDK框架分析
  5. java breakpoint_java断点
  6. 基于2440的Linux开发原理,基于S3C2440和Linux的嵌入式网络驱动程序开发
  7. 如何查看mongo集合的数据类型
  8. linux x中文显示,01_Linux系统系统语言查询,设置Xshell工具,中文显示,测试Xshell中文字符显示,Linux中文显示乱码设置...
  9. 畅想未来计算机300字,畅想未来作文300字
  10. 1688api 图片搜索功能 item_search_img-按图搜索1688商品(拍立淘)
  11. TL431-2.5v基准电压芯片几种基本用法
  12. 【Vue】实现出生日期计算年龄
  13. 帖子—评论的数据库设计和代码实现(思路记录)
  14. 菜鸟的Springboot学习日历(一)
  15. Mac 快速查找快捷键command+f失效解决办法
  16. 群晖黑科技docker套件_群晖Docker玩法
  17. 观影《超时空接触》有感
  18. 情景模拟面试真题解析
  19. 用AI生成的画作,在淘宝拍卖到了4位数
  20. 最简单的理财:定投与资产配置

热门文章

  1. Linux:initrd:dracut, dracut-pre-udev; initramfs启动
  2. CocosCreater3.0热更新
  3. 你必须要知道的20个救命小常识
  4. Python 编码为什么那么蛋疼,蓝瘦,香菇?
  5. 那些微小的改变,让我们越来越好
  6. IDEA构建maven项目生成的文件详解 (.mvn、mvnw、mvnw.cmd、.gitignore、.iml、.idea、pom.xml)
  7. Gitlib cicd 流水线 mvnw Permission denied 问题处理
  8. 【面经】中邮消费金融大数据开发面经
  9. git 几种还原版本_git的几种回滚 git revert 和 git reset的区别
  10. 论文《Deep learning for steganalysis via convolutional neural》解读