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++ 函數實現擁有與頭文件中的函數說明相同的說明。
參考文獻: