android開發教程(3)— jni編程之採用SWIG從Java調用C/C++

                                           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

須知:SWIG和javah的區別(強烈推薦)

我看了網上的關於 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++ 的五個步驟

安卓開發中,從 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 (看)

相關文章
相關標籤/搜索